use std::collections::BTreeMap;
use rustev_contract::Document;
use rustev_contract::canonical::{CanonicalError, record_canonical_bytes, tagged_digest};
use rustev_contract::eval_report::Split;
use rustev_contract::ids::{ContentDigest, DatasetId, EvaluatorConfigId, PlanId};
use rustev_contract::judgment::{Derivation, Notice, OutValue};
use rustev_contract::limits::REPLAY_V1;
use serde::{Deserialize, Serialize};
use crate::dataset::Label;
use crate::gold::Gold;
use crate::scoring::{ScorerDescriptor, ScorerOutput};
pub const SCORE_VIEWS: &str = "rustev.score-views/1";
pub const SCORES: &str = "rustev.scores/1";
pub const PROPOSAL_VIEW: &str = "rustev.proposal-view/1";
pub mod reserved {
pub const INVALID_SCORE: &str = "invalid_score";
pub const JUDGE_BUNDLE_MISSING: &str = "judge_bundle_missing";
pub const JUDGE_NOT_REPRODUCED: &str = "judge_not_reproduced";
pub const SUBJECT_MISMATCH: &str = "subject_mismatch";
pub const JUDGE_UNRESOLVED: &str = "judge_unresolved";
pub const SCORER_FAILED: &str = "scorer_failed";
pub const SCORER_OVERLOADED: &str = "scorer_overloaded";
pub const SCORER_DEADLINE: &str = "scorer_deadline";
pub const ALL: [&str; 8] = [
INVALID_SCORE,
JUDGE_BUNDLE_MISSING,
JUDGE_NOT_REPRODUCED,
SUBJECT_MISMATCH,
JUDGE_UNRESOLVED,
SCORER_FAILED,
SCORER_OVERLOADED,
SCORER_DEADLINE,
];
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScoreViews {
pub schema: String,
pub dataset: DatasetId,
pub split: Split,
pub config: EvaluatorConfigId,
pub plan: PlanId,
pub cases: Vec<ViewEntry>,
}
crate::doc::eval_document!(ScoreViews, SCORE_VIEWS, REPLAY_V1);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ViewEntry {
pub case: String,
pub case_view: CaseView,
pub proposal_view: ProposalView,
pub subject: ContentDigest,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CaseView {
pub id: String,
pub label: Label,
pub gold: Option<Gold>,
pub subgroups: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProposalView {
pub action: String,
pub params: BTreeMap<String, OutValue>,
pub derivation: Derivation,
pub notices: Vec<Notice>,
}
impl ProposalView {
pub fn subject(&self) -> Result<ContentDigest, CanonicalError> {
let digest = tagged_digest(PROPOSAL_VIEW, &record_canonical_bytes(self)?);
Ok(ContentDigest::parse(&digest).expect("a tagged digest is a content digest"))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Scores {
pub schema: String,
pub scorer: ScorerDescriptor,
pub views: ContentDigest,
pub entries: Vec<ScoreEntry>,
}
crate::doc::eval_document!(Scores, SCORES, REPLAY_V1);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScoreEntry {
pub case: String,
pub subject: ContentDigest,
pub outcome: ScoreOutcome,
pub judge_run: Option<ContentDigest>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScoreOutcome {
Score(f64),
Unscored(String),
}
pub trait Scorer: Send + Sync {
fn descriptor(&self) -> &ScorerDescriptor;
fn score(&self, case: &CaseView, proposal: &ProposalView) -> ScoreOutcome;
}
pub fn reason_ok(r: &str) -> bool {
(1..=64).contains(&r.len())
&& r.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
pub fn score_ok(output: &ScorerOutput, value: f64) -> bool {
match output {
ScorerOutput::Ordinal { levels } => {
value.is_finite() && value.fract() == 0.0 && value >= 0.0 && value < levels.len() as f64
}
ScorerOutput::Bounded { min, max } => {
value.is_finite() && value >= min.to_f64_nearest() && value <= max.to_f64_nearest()
}
}
}
pub fn scorer_case(d: &ScorerDescriptor, case: &CaseView) -> CaseView {
CaseView {
gold: if d.reads_gold {
case.gold.clone()
} else {
None
},
..case.clone()
}
}
pub fn checked(d: &ScorerDescriptor, outcome: ScoreOutcome) -> ScoreOutcome {
match outcome {
ScoreOutcome::Score(v) if score_ok(&d.output, v) => ScoreOutcome::Score(v),
ScoreOutcome::Unscored(r) if reason_ok(&r) && !reserved::ALL.contains(&r.as_str()) => {
ScoreOutcome::Unscored(r)
}
_ => ScoreOutcome::Unscored(reserved::INVALID_SCORE.into()),
}
}
impl ScoreEntry {
pub fn of(entry: &ViewEntry, outcome: ScoreOutcome) -> ScoreEntry {
ScoreEntry {
case: entry.case.clone(),
subject: entry.subject.clone(),
outcome,
judge_run: None,
}
}
}
impl Scores {
pub fn over(
d: &ScorerDescriptor,
views: &ScoreViews,
entries: Vec<ScoreEntry>,
) -> Result<Scores, CanonicalError> {
Ok(Scores {
schema: SCORES.into(),
scorer: d.clone(),
views: views.record_digest()?,
entries,
})
}
}
pub fn score_views(scorer: &dyn Scorer, views: &ScoreViews) -> Result<Scores, CanonicalError> {
let d = scorer.descriptor();
let entries = views
.cases
.iter()
.map(|e| {
let outcome = scorer.score(&scorer_case(d, &e.case_view), &e.proposal_view);
ScoreEntry::of(e, checked(d, outcome))
})
.collect();
Scores::over(d, views, entries)
}