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 PlanStepDetail::Rank(b) => &b.artifact,
393 _ => return None,
394 };
395 if target == 0 {
396 return Some(artifact.clone());
397 }
398 match &plan.execution {
399 PlanExecution::Declared(d) => d
400 .fallbacks
401 .iter()
402 .find(|f| f.step == step && f.target == target)
403 .map(|f| f.artifact.clone()),
404 PlanExecution::None => None,
405 }
406}
407
408enum ReplayEvaluation<'a> {
410 Decision(Box<Evaluation<'a>>),
411 Generation(Box<GenerationEvaluation<'a>>),
412}
413impl<'a> ReplayEvaluation<'a> {
414 fn advance(self, decision: &str) -> Result<Self, Incomparable> {
415 match self {
420 Self::Decision(ev)
421 if ev.pending().is_empty() && ev.pending_acquisitions().is_empty() =>
422 {
423 ev.begin_generation(decision)
424 .map(Box::new)
425 .map(Self::Generation)
426 .map_err(|_| inconsistent(ItemLocation::Bundle, Inconsistency::FinishRefused))
427 }
428 other => Ok(other),
429 }
430 }
431 fn pending(&self) -> Vec<(String, Vec<String>)> {
432 match self {
433 Self::Decision(ev) => ev
434 .pending()
435 .iter()
436 .map(|r| (r.step.clone(), r.instance.clone()))
437 .collect(),
438 Self::Generation(g) => g
439 .pending()
440 .iter()
441 .map(|r| (r.document.step.clone(), r.document.instance.clone()))
442 .collect(),
443 }
444 }
445 fn request_document(
446 &self,
447 step: &str,
448 instance: &[String],
449 target: u32,
450 scope: &Scope,
451 ) -> Result<rustev_contract::request::RequestDoc, rustev_core::RequestIdentityError> {
452 match self {
453 Self::Decision(ev) => ev.request_document(step, instance, target, scope),
454 Self::Generation(g) => g.request_document(step, instance, target, scope),
455 }
456 }
457 fn accepts(&self, step: &str, instance: &[String], output: &RawOutput) -> bool {
458 match self {
459 Self::Decision(ev) => matches!(ev.check_output(step, instance, output), Ok(Ok(()))),
460 Self::Generation(g) => matches!(g.check_output(step, instance, output), Ok(Ok(()))),
461 }
462 }
463 fn supply(
464 &mut self,
465 step: &str,
466 instance: &[String],
467 value: Supplied,
468 q: &rustev_contract::run::RequestRecord,
469 decision: &str,
470 ) -> bool {
471 match self {
472 Self::Decision(ev) => ev.supply(step, instance, value).is_ok(),
473 Self::Generation(g) => {
474 let from = q.supplied_from().expect("checked supplied request");
477 let attempt = from
478 .attempt
479 .map(|a| a.attempt_id.as_str())
480 .or(from.reused_from);
481 let fallback = format!("{decision}/{step}/refused");
482 let attempt = attempt.unwrap_or(&fallback);
483 if g.supply_attempt(
484 step,
485 instance,
486 value,
487 attempt,
488 (q.attempts.len() as u32).max(1),
489 )
490 .is_err()
491 {
492 return false;
493 }
494 if let Some(source) = from.reused_from {
495 let decision = source
496 .split('/')
497 .next()
498 .unwrap_or("")
499 .replace("%2F", "/")
500 .replace("%2C", ",")
501 .replace("%25", "%");
502 g.note_cached_attempt(attempt, &decision);
503 }
504 true
505 }
506 }
507 }
508}
509
510fn list_new(
512 ev: &ReplayEvaluation<'_>,
513 listed: &mut Vec<(String, Vec<String>)>,
514 seen: &mut BTreeSet<(String, Vec<String>)>,
515) {
516 for key in ev.pending() {
517 if seen.insert(key.clone()) {
518 listed.push(key);
519 }
520 }
521}
522
523fn reproduce_inner(
524 bundle: &ReplayBundle,
525 resolver: &dyn Resolver,
526 config: &ReplayConfig,
527) -> Result<ReplayOutcome, Incomparable> {
528 if config.scope != bundle.scope {
530 return Err(Incomparable::ScopeMismatch);
531 }
532 bundle
533 .check(config.host_cap_ms)
534 .map_err(Incomparable::InvalidBundle)?;
535 if bundle.capture != CaptureStatus::Complete {
536 return Err(Incomparable::CaptureIncomplete(bundle.capture));
537 }
538 let r = resolve_all(bundle, resolver, config.now_ms)?;
540 let run = &r.run;
541 let run_at = ItemLocation::Run;
542 if run.decision_id != bundle.decision_id {
543 return Err(inconsistent(run_at, Inconsistency::DecisionId));
544 }
545 if run.plan_id != bundle.plan_id {
546 return Err(inconsistent(run_at, Inconsistency::PlanId));
547 }
548 let evidence = match (&run.termination, &run.core) {
549 (Termination::Judged, CoreEvidence::Recorded(e)) => e,
550 _ => return Err(Incomparable::NoExpectedJudgment),
551 };
552 if evidence.decision_id != bundle.decision_id {
553 return Err(inconsistent(run_at, Inconsistency::DecisionId));
554 }
555 if evidence.plan_id != bundle.plan_id || evidence.judgment.plan_id != bundle.plan_id {
556 return Err(inconsistent(run_at, Inconsistency::PlanId));
557 }
558 if evidence.snapshot_id != bundle.snapshot_id {
559 return Err(inconsistent(run_at, Inconsistency::SnapshotId));
560 }
561 if evidence.evaluation_time_ms != bundle.evaluation_time_ms {
562 return Err(inconsistent(run_at, Inconsistency::EvaluationTime));
563 }
564 if r.plan.id().ok().as_ref() != Some(&bundle.plan_id) {
565 return Err(inconsistent(ItemLocation::Plan, Inconsistency::PlanId));
566 }
567 if r.snapshot.id().ok().as_ref() != Some(&bundle.snapshot_id) {
568 return Err(inconsistent(
569 ItemLocation::Snapshot,
570 Inconsistency::SnapshotId,
571 ));
572 }
573 let max = r
574 .plan
575 .definition
576 .limits
577 .max_semantic_requests
578 .min(MAX_BUNDLE_ITEMS as u64);
579 if bundle.supplies.len() as u64 > max {
580 return Err(inconsistent(
581 ItemLocation::Bundle,
582 Inconsistency::TooManySupplies { max },
583 ));
584 }
585 let max_pages = r
586 .plan
587 .definition
588 .limits
589 .acquisition
590 .as_ref()
591 .map_or(0, |l| l.max_calls);
592 if r.pages.len() as u64 > max_pages {
593 return Err(inconsistent(
594 ItemLocation::Bundle,
595 Inconsistency::TooManySupplies { max: max_pages },
596 ));
597 }
598 let compiled =
600 Compiled::load_checked_with_sources(&r.plan, &r.descriptors, &r.calibrations, &r.sources)
601 .map_err(|e| match e {
602 LoadError::CompilerChanged {
603 recorded_compiler,
604 current_compiler,
605 recorded_registry,
606 current_registry,
607 } => Incomparable::CompilerChanged {
608 recorded_compiler,
609 current_compiler: current_compiler.into(),
610 recorded_registry,
611 current_registry: current_registry.into(),
612 },
613 e @ (LoadError::MissingDescriptor { .. } | LoadError::MissingCalibration { .. }) => {
614 Incomparable::MissingDependency {
615 detail: e.to_string(),
616 }
617 }
618 LoadError::PlanMismatch { .. } => Incomparable::PlanMismatch,
619 })?;
620 let ev = Evaluation::start(&compiled, &r.snapshot, bundle.evaluation_time_ms)
622 .map_err(|_| inconsistent(ItemLocation::Snapshot, Inconsistency::StartRefused))?;
623 let mut ev = ReplayEvaluation::Decision(Box::new(ev)).advance(&bundle.decision_id)?;
624 let by_key: BTreeMap<(&str, &[String]), usize> = run
625 .requests
626 .iter()
627 .enumerate()
628 .map(|(i, q)| ((q.step.as_str(), q.instance.as_slice()), i))
629 .collect();
630 let mut listed = vec![];
631 let mut seen = BTreeSet::new();
632 list_new(&ev, &mut listed, &mut seen);
633 let mut supplies = vec![];
634 let pages = bundle.acquisition_pages.as_deref().unwrap_or(&[]);
635 let mut missing = vec![];
636 let (mut semantic_index, mut page_index) = (0, 0);
637 loop {
638 if let ReplayEvaluation::Decision(decision) = &mut ev {
639 fill_missing(decision, &pages[page_index..], &mut missing)?;
640 }
641 ev = ev.advance(&bundle.decision_id)?;
642 list_new(&ev, &mut listed, &mut seen);
643 let semantic = bundle.supplies.get(semantic_index);
644 let page = pages.get(page_index);
645 if let Some(page) = page
646 && semantic.is_none_or(|s| page.sequence < s.sequence)
647 {
648 let ReplayEvaluation::Decision(decision) = &mut ev else {
650 if !missing.is_empty() {
651 page_index += 1;
652 continue;
653 }
654 return Err(inconsistent(
655 ItemLocation::AcquisitionPage(page_index),
656 Inconsistency::NotPending,
657 ));
658 };
659 if missing.is_empty()
660 || decision.pending_acquisitions().iter().any(|p| {
661 p.step == page.step && p.tier_index == page.tier && p.page_index == page.page
662 })
663 {
664 supply_page(decision, page, &r.pages[page_index], run, page_index)?;
665 }
666 page_index += 1;
667 continue;
668 }
669 let Some(entry) = semantic else {
670 break;
671 };
672 let i = semantic_index;
673 let value = &r.values[i];
674 semantic_index += 1;
675 if !missing.is_empty()
676 && !ev
677 .pending()
678 .iter()
679 .any(|p| p.0 == entry.step && p.1 == entry.instance)
680 {
681 continue;
682 }
683 let at = ItemLocation::Supply(i);
684 let bad = |what| Err(inconsistent(at, what));
685 let Some(q) = by_key
686 .get(&(entry.step.as_str(), entry.instance.as_slice()))
687 .map(|&k| &run.requests[k])
688 else {
689 return bad(Inconsistency::NotInRun);
690 };
691 let Some(from) = q.supplied_from() else {
692 return bad(Inconsistency::NotInRun);
693 };
694 let origin = from
697 .attempt
698 .map(|a| a.attempt_id.as_str())
699 .or(from.reused_from);
700 let output_target = from
701 .attempt
702 .map(|a| a.target)
703 .or(from.reused_from.map(|_| from.target));
704 if from.target != entry.target
705 || origin != entry.attempt_id.as_deref()
706 || matches!(q.result, RequestResult::Output { .. })
707 && output_target != Some(entry.target)
708 {
709 return bad(Inconsistency::Origin);
710 }
711 let doc =
712 match ev.request_document(&entry.step, &entry.instance, entry.target, &bundle.scope) {
713 Ok(d) => d,
714 Err(rustev_core::RequestIdentityError::UnknownTarget { .. }) => {
715 return bad(Inconsistency::UnknownTarget);
716 }
717 Err(_) => return bad(Inconsistency::NotPending),
718 };
719 if doc.id().ok().as_ref() != Some(&entry.request) {
720 return bad(Inconsistency::RequestIdentity);
721 }
722 let (supplied, retained) = match (value, &q.result) {
723 (Value::Output(o), RequestResult::Output { target }) if *target == entry.target => {
724 let bound = target_artifact(&r.plan, &entry.step, entry.target);
725 if o.step != entry.step
726 || o.instance != entry.instance
727 || bound.as_ref() != Some(&o.artifact)
728 || from.attempt.is_some_and(|a| a.artifact != o.artifact)
729 {
730 return bad(Inconsistency::OutputBinding);
731 }
732 if !ev.accepts(&entry.step, &entry.instance, &o.output) {
734 return bad(Inconsistency::OutputRefused);
735 }
736 (
737 Supplied::Output(o.output.clone()),
738 RetainedValue::Output {
739 artifact: o.artifact.clone(),
740 output: o.output.clone(),
741 },
742 )
743 }
744 (Value::Reason(d), RequestResult::Failed(u)) if &d.reason == u => (
745 Supplied::Failed(d.reason.clone()),
746 RetainedValue::Reason(d.reason.clone()),
747 ),
748 _ => return bad(Inconsistency::Value),
749 };
750 if let Some(context) = entry.failure_context
751 && let ReplayEvaluation::Decision(decision) = &mut ev
752 {
753 decision.note_failure(
754 &entry.step,
755 &entry.instance,
756 rustev_core::FailureContext {
757 origin: context.origin,
758 determinism: context.determinism,
759 },
760 );
761 }
762 if !ev.supply(
763 &entry.step,
764 &entry.instance,
765 supplied,
766 q,
767 &bundle.decision_id,
768 ) {
769 return bad(Inconsistency::NotPending);
770 }
771 ev = ev.advance(&bundle.decision_id)?;
772 list_new(&ev, &mut listed, &mut seen);
773 supplies.push(RetainedSupply {
774 step: entry.step.clone(),
775 instance: entry.instance.clone(),
776 request: entry.request.clone(),
777 target: entry.target,
778 value: retained,
779 });
780 }
781 if let Some(p) = ev.pending().first() {
782 return Err(inconsistent(
783 ItemLocation::Bundle,
784 Inconsistency::MissingSupply {
785 step: p.0.clone(),
786 instance: p.1.clone(),
787 },
788 ));
789 }
790 let recorded: Vec<(String, Vec<String>)> = run
791 .requests
792 .iter()
793 .map(|q| (q.step.clone(), q.instance.clone()))
794 .collect();
795 if missing.is_empty() && recorded != listed {
796 return Err(inconsistent(run_at, Inconsistency::RequestOrder));
797 }
798 let ReplayEvaluation::Generation(stage) = ev else {
800 return Err(inconsistent(
801 ItemLocation::Bundle,
802 Inconsistency::FinishRefused,
803 ));
804 };
805 let (judgment, actual_evidence, text_result) = stage
806 .finish()
807 .map_err(|_| inconsistent(ItemLocation::Bundle, Inconsistency::FinishRefused))?;
808 if !missing.is_empty() {
809 return Err(Incomparable::AcquisitionNotCaptured {
810 missing,
811 judgment: Box::new(judgment),
812 });
813 }
814 let expected = &evidence.judgment;
815 let same = match (judgment.record_canonical(), expected.record_canonical()) {
816 (Ok(a), Ok(b)) => a == b,
817 _ => {
818 return Err(inconsistent(ItemLocation::Run, Inconsistency::NoRecordForm));
819 }
820 };
821 let generation_bytes =
823 |g: &Option<Vec<rustev_contract::generation_result::GenerationEvidence>>| {
824 let mut g = g.clone();
825 if let Some(steps) = &mut g {
826 for step in steps {
827 step.notices.retain(|n| {
828 matches!(
829 n.kind,
830 rustev_contract::generation_result::TextNoticeKind::BackstopTruncated
831 )
832 });
833 }
834 }
835 rustev_contract::canonical::record_canonical_bytes(&g)
836 };
837 let generation_same = match (
838 generation_bytes(&actual_evidence.generation),
839 generation_bytes(&evidence.generation),
840 ) {
841 (Ok(a), Ok(b)) => a == b,
842 _ => return Err(inconsistent(ItemLocation::Run, Inconsistency::NoRecordForm)),
843 };
844 if !same || !generation_same || actual_evidence.screening != evidence.screening {
845 return Ok(ReplayOutcome::Diverged {
846 expected: Box::new(expected.clone()),
847 actual: Box::new(judgment),
848 });
849 }
850 Ok(ReplayOutcome::Reproduced(Box::new(Reproduced {
851 decision_id: bundle.decision_id.clone(),
852 scope: bundle.scope.clone(),
853 plan: r.plan.clone(),
854 calibrations: r.calibrations.clone(),
855 snapshot: r.snapshot,
856 evaluation_time: bundle.evaluation_time_ms,
857 judgment,
858 run: r.run.clone(),
859 supplies,
860 pages: pages
861 .iter()
862 .zip(&r.pages)
863 .map(|(p, v)| (p.request.clone(), v.clone()))
864 .collect(),
865 text_result,
866 })))
867}