Skip to main content

rustev_eval/
replay.rs

1//! Historical reproduction from a replay bundle (spec 004, 3.3).
2//!
3//! Scope first, before any byte is resolved; then capture completeness,
4//! lifetimes, bounds, integrity and every cross-reference; then the plan is
5//! loaded with its retained dependencies and the pure core is driven with
6//! the retained snapshot, the recorded evaluation time and the retained
7//! values in actual supply order. `reproduced` means the record-canonical
8//! judgment bytes equal the retained run record's; a valid, complete replay
9//! with other bytes is `diverged`; anything else is `incomparable` with a
10//! typed reason and location. No backend is called: there is none here.
11//!
12//! Nothing digests the bundle's own scope field: relabeling is caught
13//! because every supply's request identity is recomputed under the claimed
14//! scope. A bundle with no supplies has no identity to contradict a
15//! relabeled scope; a bundle is evidence of what the host retained, never
16//! an attestation (spec 004, 3.1.1).
17
18use 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/// The caller's trusted replay configuration.
47#[derive(Debug, Clone)]
48pub struct ReplayConfig {
49    /// The scope the caller is entitled to replay under; it must equal the
50    /// bundle's exactly, mode included.
51    pub scope: Scope,
52    /// The caller's wall-clock time for expiry, in host milliseconds.
53    pub now_ms: Timestamp,
54    /// A host cap on bundle lifetime, tighter than the hard 30 days.
55    pub host_cap_ms: Option<u64>,
56}
57
58/// A cross-reference that does not hold.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Inconsistency {
61    DecisionId,
62    PlanId,
63    SnapshotId,
64    EvaluationTime,
65    /// More supplies than the plan can issue.
66    TooManySupplies {
67        max: u64,
68    },
69    /// The entry's request is not pending when its turn comes.
70    NotPending,
71    /// The run record has no such request, or did not supply it.
72    NotInRun,
73    /// The entry's target or attempt is not the run record's.
74    Origin,
75    /// The target is not bound by the plan for this step.
76    UnknownTarget,
77    /// The recomputed request identity differs from the entry's.
78    RequestIdentity,
79    /// An output where the run supplied a reason, or the reverse, or a
80    /// reason other than the recorded one.
81    Value,
82    /// The output document names another step, instance or artifact.
83    OutputBinding,
84    /// The core refuses the retained output the run supplied.
85    OutputRefused,
86    /// A request the run supplied has no entry.
87    MissingSupply {
88        step: String,
89        instance: Vec<String>,
90    },
91    /// The requests were not listed in the recorded order.
92    RequestOrder,
93    /// The core refused to start on the retained snapshot.
94    StartRefused,
95    /// The core could not finish once every entry was supplied.
96    FinishRefused,
97    /// A judgment has no record canonical form.
98    NoRecordForm,
99}
100
101/// Why a case cannot be reproduced or compared.
102#[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    /// The run was cancelled or produced no judgment.
119    NoExpectedJudgment,
120    CompilerChanged {
121        recorded_compiler: String,
122        current_compiler: String,
123        recorded_registry: String,
124        current_registry: String,
125    },
126    /// The plan binds a descriptor or calibration the bundle does not carry.
127    MissingDependency {
128        detail: String,
129    },
130    /// Same compiler, but the plan does not recompile identically.
131    PlanMismatch,
132    Inconsistent {
133        location: ItemLocation,
134        what: Inconsistency,
135    },
136}
137
138impl Incomparable {
139    /// A stable name for counting cases by reason.
140    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/// A retained value, as reproduction supplied it.
179#[derive(Debug, Clone, PartialEq)]
180pub enum RetainedValue {
181    Output {
182        artifact: ArtifactId,
183        output: RawOutput,
184    },
185    /// A historical runtime observation, never a backend output.
186    Reason(Unresolved),
187}
188
189/// One checked supply entry.
190#[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/// A case whose judgment reproduced byte for byte, with what candidate
200/// comparison needs. Historical timing, attempts and cost stay in the run
201/// record: copied observations, never remeasured. Only [`reproduce`]
202/// constructs one, so a candidate is only ever compared on a reproduced
203/// case (spec 004, 3.4.4).
204#[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    /// Reconstructed generation text, never dispatched or fetched from a cache.
221    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    /// The retained plan and its calibrations, as loaded.
232    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    /// The reproduced judgment, byte-equal to the retained one.
245    pub fn judgment(&self) -> &Judgment {
246        &self.judgment
247    }
248    /// The retained run record: historical observations.
249    pub fn run(&self) -> &RunRecord {
250        &self.run
251    }
252    /// The checked supply entries, in supply order.
253    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
282/// Reproduce one retained decision.
283pub 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
383/// The target binding of a step: artifact of target 0 or of a fallback.
384fn 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
407// The transition freezes selection before generation, just as runtime does.
408enum 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        // Plans 2 and 3 without generation enter an empty stage whose finish
415        // preserves judgment and evidence. Covered by
416        // instantiated_plan_assembles_and_replays_without_inference and
417        // support_routing_reproduces_offline_without_inference.
418        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                // reproduce_inner rejects a missing supplied_from as NotInRun
474                // before calling supply with this same immutable record.
475                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
509/// Append newly pending requests in the core's listing order.
510fn 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    // 1. Scope, before anything is resolved.
528    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    // 2. Every retained document, under its own limits and the case cap.
538    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    // 3. The plan, with its retained dependencies only.
598    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    // 4. The pure core, fed in actual supply order.
620    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            // Missing pages can make a later retained tier unreachable.
648            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        // An output reused from a cache entry names the earlier attempt that
694        // produced it; the record itself made no attempt (spec 017, 3.8).
695        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                // The run supplied it, so the same core accepts it.
732                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    // 5. The verdict.
798    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    // Ownership notices are runtime observations, not reproduction inputs.
821    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}