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        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
408// The transition freezes selection before generation, just as runtime does.
409enum 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        // Plans 2 and 3 without generation enter an empty stage whose finish
416        // preserves judgment and evidence. Covered by
417        // instantiated_plan_assembles_and_replays_without_inference and
418        // support_routing_reproduces_offline_without_inference.
419        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                // reproduce_inner rejects a missing supplied_from as NotInRun
475                // before calling supply with this same immutable record.
476                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
510/// Append newly pending requests in the core's listing order.
511fn 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    // 1. Scope, before anything is resolved.
529    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    // 2. Every retained document, under its own limits and the case cap.
539    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    // 3. The plan, with its retained dependencies only.
599    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    // 4. The pure core, fed in actual supply order.
621    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            // Missing pages can make a later retained tier unreachable.
649            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        // An output reused from a cache entry names the earlier attempt that
695        // produced it; the record itself made no attempt (spec 017, 3.8).
696        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                // The run supplied it, so the same core accepts it.
733                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    // 5. The verdict.
799    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    // Ownership notices are runtime observations, not reproduction inputs.
822    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}