use std::collections::BTreeMap;
use std::fmt;
use rustev_contract::Document;
use rustev_contract::ids::PlanId;
use rustev_contract::judgment::{Derivation, OutValue, Outcome};
use serde_json::Value;
use crate::bundle::BundleInput;
use crate::replay::{ReplayConfig, ReplayOutcome, reproduce};
use crate::resolve::Resolver;
use crate::scores::{SCORES, ScoreEntry, ScoreOutcome, ScoreViews, Scores, reserved};
use crate::scoring::{ScorerDescriptor, ScorerKind, ScorerOutput};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JudgeError {
NotAJudge(String),
PlanMismatch {
case: String,
},
DerivationMismatch {
case: String,
},
NotCanonical,
}
impl JudgeError {
pub fn code(&self) -> &'static str {
match self {
JudgeError::NotAJudge(_) => "not-a-judge",
JudgeError::PlanMismatch { .. } => "judge-plan-mismatch",
JudgeError::DerivationMismatch { .. } => "judge-derivation-mismatch",
JudgeError::NotCanonical => "not-canonical",
}
}
}
impl fmt::Display for JudgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {self:?}", self.code())
}
}
impl std::error::Error for JudgeError {}
pub fn judge_scores(
views: &ScoreViews,
descriptor: &ScorerDescriptor,
plan: &PlanId,
bundles: &BTreeMap<String, BundleInput>,
resolver: &dyn Resolver,
replay: &ReplayConfig,
) -> Result<Scores, JudgeError> {
let (Some(judge), ScorerKind::Model, ScorerOutput::Ordinal { levels }) =
(&descriptor.judge, descriptor.kind, &descriptor.output)
else {
return Err(JudgeError::NotAJudge(
"a judge is an ordinal model scorer with a judge reference".into(),
));
};
if &judge.plan != plan {
return Err(JudgeError::NotAJudge(
"the descriptor names another judge plan".into(),
));
}
let mut entries = Vec::with_capacity(views.cases.len());
for e in &views.cases {
let unscored = |reason: &str| ScoreEntry {
case: e.case.clone(),
subject: e.subject.clone(),
outcome: ScoreOutcome::Unscored(reason.into()),
judge_run: None,
};
let bundle = match bundles.get(&e.case) {
None => {
entries.push(unscored(reserved::JUDGE_BUNDLE_MISSING));
continue;
}
Some(BundleInput::Failed(_)) => {
entries.push(unscored(reserved::JUDGE_NOT_REPRODUCED));
continue;
}
Some(BundleInput::Loaded(b)) => b,
};
if &bundle.plan_id != plan {
return Err(JudgeError::PlanMismatch {
case: e.case.clone(),
});
}
let r = match reproduce(bundle, resolver, replay) {
ReplayOutcome::Reproduced(r) => r,
ReplayOutcome::Diverged { .. } | ReplayOutcome::Incomparable(_) => {
entries.push(unscored(reserved::JUDGE_NOT_REPRODUCED));
continue;
}
};
let subject = Value::String(e.subject.to_string());
let supplied = r
.snapshot()
.entries
.iter()
.find(|x| x.field == judge.subject_field)
.map(|x| &x.value);
if supplied != Some(&subject) {
entries.push(unscored(reserved::SUBJECT_MISMATCH));
continue;
}
let j = r.judgment();
if j.derivation == Derivation::ExactDerived {
return Err(JudgeError::DerivationMismatch {
case: e.case.clone(),
});
}
let judge_run = Some(
r.run()
.record_digest()
.map_err(|_| JudgeError::NotCanonical)?,
);
let outcome = match &j.outcome {
Outcome::Propose { params, .. } => match params.get(&judge.level_param) {
Some(OutValue::Enum(level)) => match levels.iter().position(|l| l == level) {
Some(i) => ScoreOutcome::Score(i as f64),
None => ScoreOutcome::Unscored(reserved::INVALID_SCORE.into()),
},
_ => ScoreOutcome::Unscored(reserved::INVALID_SCORE.into()),
},
_ => ScoreOutcome::Unscored(reserved::JUDGE_UNRESOLVED.into()),
};
entries.push(ScoreEntry {
case: e.case.clone(),
subject: e.subject.clone(),
outcome,
judge_run,
});
}
Ok(Scores {
schema: SCORES.into(),
scorer: descriptor.clone(),
views: views
.record_digest()
.map_err(|_| JudgeError::NotCanonical)?,
entries,
})
}