use std::collections::BTreeMap;
use crate::acquisition_replay::{MissingAcquisition, PageValue};
use rustev_contract::canonical::record_canonical_bytes;
use rustev_contract::ids::RequestId;
use rustev_contract::judgment::Judgment;
use rustev_contract::output::RawOutput;
use rustev_contract::{Document, Identified};
use rustev_core::Compiled;
use rustev_core::evaluate::{Evaluation, Supplied};
use crate::replay::{Reproduced, RetainedValue};
#[derive(Debug, Clone, PartialEq)]
pub enum CandidateIncomparable {
AcquisitionNotCaptured {
missing: Vec<MissingAcquisition>,
judgment: Box<Judgment>,
},
SnapshotRefused { detail: String },
RequestMismatch { step: String, instance: Vec<String> },
HistoricalRuntimeFailure { step: String, instance: Vec<String> },
AmbiguousOutput { step: String, instance: Vec<String> },
OutputRefused { step: String, instance: Vec<String> },
}
impl CandidateIncomparable {
pub fn code(&self) -> &'static str {
match self {
CandidateIncomparable::AcquisitionNotCaptured { .. } => "acquisition-not-captured",
CandidateIncomparable::SnapshotRefused { .. } => "snapshot-refused",
CandidateIncomparable::RequestMismatch { .. } => "request-mismatch",
CandidateIncomparable::HistoricalRuntimeFailure { .. } => "historical-runtime-failure",
CandidateIncomparable::AmbiguousOutput { .. } => "ambiguous-output",
CandidateIncomparable::OutputRefused { .. } => "output-refused",
}
}
}
#[derive(Debug, Clone)]
pub struct Compared {
pub historical: Judgment,
pub candidate: Judgment,
pub reused: usize,
pub supplied: Vec<(String, Vec<String>, RawOutput)>,
}
pub fn compare(case: &Reproduced, candidate: &Compiled) -> Result<Compared, CandidateIncomparable> {
let mut pool: BTreeMap<&RequestId, Vec<&RetainedValue>> = BTreeMap::new();
for s in &case.supplies {
pool.entry(&s.request).or_default().push(&s.value);
}
let mut ev =
Evaluation::start(candidate, &case.snapshot, case.evaluation_time).map_err(|e| {
CandidateIncomparable::SnapshotRefused {
detail: e.to_string(),
}
})?;
let mut reused = 0;
let mut supplied = vec![];
let mut missing = vec![];
loop {
let pages = ev.pending_acquisitions();
let had_pages = !pages.is_empty();
for p in pages {
let outputs: Vec<_> = case
.pages
.iter()
.filter_map(|(id, v)| match v {
PageValue::Page(page) if id == &p.request_id => Some(page),
_ => None,
})
.collect();
if let Some(first) = outputs.first() {
let bytes =
first
.record_canonical()
.map_err(|_| CandidateIncomparable::OutputRefused {
step: p.step.clone(),
instance: vec![],
})?;
if outputs
.iter()
.any(|v| v.record_canonical().ok().as_ref() != Some(&bytes))
{
return Err(CandidateIncomparable::AmbiguousOutput {
step: p.step,
instance: vec![],
});
}
ev.supply_page(&p.step, p.tier_index, p.page_index, &bytes)
.map_err(|_| CandidateIncomparable::OutputRefused {
step: p.step.clone(),
instance: vec![],
})?;
reused += 1;
} else {
ev.supply_acquisition_failure(
&p.step,
p.tier_index,
p.page_index,
rustev_contract::judgment::Unresolved::MissingEvidence {
fields: vec![p.step.clone()],
},
)
.map_err(|_| CandidateIncomparable::OutputRefused {
step: p.step.clone(),
instance: vec![],
})?;
missing.push(MissingAcquisition {
step: p.step,
tier: p.tier_index,
page: p.page_index,
request: p.request_id,
});
}
}
let pending = ev.pending();
if pending.is_empty() {
if had_pages {
continue;
}
break;
}
for r in pending {
let (step, instance) = (r.step.clone(), r.instance.clone());
let id = ev
.request_document(&r.step, &r.instance, 0, &case.scope)
.ok()
.and_then(|d| d.id().ok());
let retained = id.as_ref().and_then(|i| pool.get(i));
let outputs: Vec<&RawOutput> = retained
.into_iter()
.flatten()
.filter_map(|v| match v {
RetainedValue::Output { output, .. } => Some(output),
RetainedValue::Reason(_) => None,
})
.collect();
let Some(first) = outputs.first() else {
return Err(if retained.is_some_and(|v| !v.is_empty()) {
CandidateIncomparable::HistoricalRuntimeFailure { step, instance }
} else {
CandidateIncomparable::RequestMismatch { step, instance }
});
};
if !agree(retained.into_iter().flatten()) {
return Err(CandidateIncomparable::AmbiguousOutput { step, instance });
}
if !matches!(ev.check_output(&step, &instance, first), Ok(Ok(()))) {
return Err(CandidateIncomparable::OutputRefused { step, instance });
}
ev.supply(&step, &instance, Supplied::Output((*first).clone()))
.map_err(|_| CandidateIncomparable::RequestMismatch {
step: step.clone(),
instance: instance.clone(),
})?;
reused += 1;
supplied.push((step, instance, (*first).clone()));
}
}
let (candidate, _) = ev
.finish(&case.decision_id)
.expect("every pending request was supplied");
if !missing.is_empty() {
return Err(CandidateIncomparable::AcquisitionNotCaptured {
missing,
judgment: Box::new(candidate),
});
}
Ok(Compared {
historical: case.judgment.clone(),
candidate,
reused,
supplied,
})
}
fn agree<'a>(values: impl Iterator<Item = &'a &'a RetainedValue>) -> bool {
let mut first: Option<Vec<u8>> = None;
for v in values {
let RetainedValue::Output { artifact, output } = v else {
continue;
};
let Ok(bytes) = record_canonical_bytes(&(artifact.clone(), output.clone())) else {
return false;
};
match &first {
None => first = Some(bytes),
Some(f) if *f == bytes => {}
Some(_) => return false,
}
}
true
}