1use std::collections::BTreeMap;
12
13use crate::acquisition_replay::{MissingAcquisition, PageValue};
14use rustev_contract::canonical::record_canonical_bytes;
15use rustev_contract::ids::RequestId;
16use rustev_contract::judgment::Judgment;
17use rustev_contract::output::RawOutput;
18use rustev_contract::{Document, Identified};
19use rustev_core::Compiled;
20use rustev_core::evaluate::{Evaluation, Supplied};
21
22use crate::replay::{Reproduced, RetainedValue};
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum CandidateIncomparable {
27 AcquisitionNotCaptured {
28 missing: Vec<MissingAcquisition>,
29 judgment: Box<Judgment>,
30 },
31 SnapshotRefused { detail: String },
33 RequestMismatch { step: String, instance: Vec<String> },
35 HistoricalRuntimeFailure { step: String, instance: Vec<String> },
37 AmbiguousOutput { step: String, instance: Vec<String> },
39 OutputRefused { step: String, instance: Vec<String> },
42}
43
44impl CandidateIncomparable {
45 pub fn code(&self) -> &'static str {
46 match self {
47 CandidateIncomparable::AcquisitionNotCaptured { .. } => "acquisition-not-captured",
48 CandidateIncomparable::SnapshotRefused { .. } => "snapshot-refused",
49 CandidateIncomparable::RequestMismatch { .. } => "request-mismatch",
50 CandidateIncomparable::HistoricalRuntimeFailure { .. } => "historical-runtime-failure",
51 CandidateIncomparable::AmbiguousOutput { .. } => "ambiguous-output",
52 CandidateIncomparable::OutputRefused { .. } => "output-refused",
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
60pub struct Compared {
61 pub historical: Judgment,
62 pub candidate: Judgment,
63 pub reused: usize,
65 pub supplied: Vec<(String, Vec<String>, RawOutput)>,
67}
68
69pub fn compare(case: &Reproduced, candidate: &Compiled) -> Result<Compared, CandidateIncomparable> {
72 let mut pool: BTreeMap<&RequestId, Vec<&RetainedValue>> = BTreeMap::new();
73 for s in &case.supplies {
74 pool.entry(&s.request).or_default().push(&s.value);
75 }
76 let mut ev =
77 Evaluation::start(candidate, &case.snapshot, case.evaluation_time).map_err(|e| {
78 CandidateIncomparable::SnapshotRefused {
79 detail: e.to_string(),
80 }
81 })?;
82 let mut reused = 0;
83 let mut supplied = vec![];
84 let mut missing = vec![];
85 loop {
86 let pages = ev.pending_acquisitions();
87 let had_pages = !pages.is_empty();
88 for p in pages {
89 let outputs: Vec<_> = case
90 .pages
91 .iter()
92 .filter_map(|(id, v)| match v {
93 PageValue::Page(page) if id == &p.request_id => Some(page),
94 _ => None,
95 })
96 .collect();
97 if let Some(first) = outputs.first() {
98 let bytes =
99 first
100 .record_canonical()
101 .map_err(|_| CandidateIncomparable::OutputRefused {
102 step: p.step.clone(),
103 instance: vec![],
104 })?;
105 if outputs
106 .iter()
107 .any(|v| v.record_canonical().ok().as_ref() != Some(&bytes))
108 {
109 return Err(CandidateIncomparable::AmbiguousOutput {
110 step: p.step,
111 instance: vec![],
112 });
113 }
114 ev.supply_page(&p.step, p.tier_index, p.page_index, &bytes)
115 .map_err(|_| CandidateIncomparable::OutputRefused {
116 step: p.step.clone(),
117 instance: vec![],
118 })?;
119 reused += 1;
120 } else {
121 ev.supply_acquisition_failure(
122 &p.step,
123 p.tier_index,
124 p.page_index,
125 rustev_contract::judgment::Unresolved::MissingEvidence {
126 fields: vec![p.step.clone()],
127 },
128 )
129 .map_err(|_| CandidateIncomparable::OutputRefused {
130 step: p.step.clone(),
131 instance: vec![],
132 })?;
133 missing.push(MissingAcquisition {
134 step: p.step,
135 tier: p.tier_index,
136 page: p.page_index,
137 request: p.request_id,
138 });
139 }
140 }
141 let pending = ev.pending();
142 if pending.is_empty() {
143 if had_pages {
144 continue;
145 }
146 break;
147 }
148 for r in pending {
149 let (step, instance) = (r.step.clone(), r.instance.clone());
150 let id = ev
151 .request_document(&r.step, &r.instance, 0, &case.scope)
152 .ok()
153 .and_then(|d| d.id().ok());
154 let retained = id.as_ref().and_then(|i| pool.get(i));
155 let outputs: Vec<&RawOutput> = retained
156 .into_iter()
157 .flatten()
158 .filter_map(|v| match v {
159 RetainedValue::Output { output, .. } => Some(output),
160 RetainedValue::Reason(_) => None,
161 })
162 .collect();
163 let Some(first) = outputs.first() else {
164 return Err(if retained.is_some_and(|v| !v.is_empty()) {
165 CandidateIncomparable::HistoricalRuntimeFailure { step, instance }
166 } else {
167 CandidateIncomparable::RequestMismatch { step, instance }
168 });
169 };
170 if !agree(retained.into_iter().flatten()) {
171 return Err(CandidateIncomparable::AmbiguousOutput { step, instance });
172 }
173 if !matches!(ev.check_output(&step, &instance, first), Ok(Ok(()))) {
176 return Err(CandidateIncomparable::OutputRefused { step, instance });
177 }
178 ev.supply(&step, &instance, Supplied::Output((*first).clone()))
179 .map_err(|_| CandidateIncomparable::RequestMismatch {
180 step: step.clone(),
181 instance: instance.clone(),
182 })?;
183 reused += 1;
184 supplied.push((step, instance, (*first).clone()));
185 }
186 }
187 let (candidate, _) = ev
188 .finish(&case.decision_id)
189 .expect("every pending request was supplied");
190 if !missing.is_empty() {
191 return Err(CandidateIncomparable::AcquisitionNotCaptured {
192 missing,
193 judgment: Box::new(candidate),
194 });
195 }
196 Ok(Compared {
197 historical: case.judgment.clone(),
198 candidate,
199 reused,
200 supplied,
201 })
202}
203
204fn agree<'a>(values: impl Iterator<Item = &'a &'a RetainedValue>) -> bool {
207 let mut first: Option<Vec<u8>> = None;
208 for v in values {
209 let RetainedValue::Output { artifact, output } = v else {
210 continue;
211 };
212 let Ok(bytes) = record_canonical_bytes(&(artifact.clone(), output.clone())) else {
213 return false;
214 };
215 match &first {
216 None => first = Some(bytes),
217 Some(f) if *f == bytes => {}
218 Some(_) => return false,
219 }
220 }
221 true
222}