rustev-eval 0.3.0

Rustev's offline evaluation: bounded replay bundles, historical reproduction, candidate comparison, datasets, metrics and calibration fitting.
Documentation
//! Pure explanations of retained decision evidence (spec 040).
mod bounds;
mod diff;
mod render;
pub use diff::explain_diff;
pub use render::render_markdown;
mod evidence;
mod excluded;
mod facts;
mod models;
mod order;
mod outcome;
mod redact;
mod scopes;
mod selected;
mod text;

use crate::resolve::Resolver;
use rustev_contract::explanation::*;
use rustev_contract::redaction::RedactionPolicy;
use rustev_contract::scope::Scope;
use rustev_contract::time::Timestamp;

pub enum Evidence<'a> {
    Record {
        record: &'a [u8],
        plan: Option<&'a [u8]>,
    },
    Bundle {
        bundle: &'a [u8],
        scope: &'a Scope,
        now_ms: Timestamp,
        resolver: &'a dyn Resolver,
    },
}
#[derive(Default)]
pub struct ExplainOptions<'a> {
    pub redaction: Option<&'a RedactionPolicy>,
}
#[derive(Debug)]
pub struct ExplainError {
    pub code: RefusalCode,
    pub subject: String,
    pub detail: String,
}
impl std::fmt::Display for ExplainError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}: {}: {}",
            self.code.as_str(),
            self.subject,
            self.detail
        )
    }
}
impl std::error::Error for ExplainError {}
impl ExplainError {
    fn new(code: RefusalCode, subject: &str, detail: impl ToString) -> Self {
        Self {
            code,
            subject: subject.into(),
            detail: detail.to_string(),
        }
    }
    fn parse(subject: &str, e: impl ToString) -> Self {
        Self::new(RefusalCode::Parse, subject, e)
    }
}
pub fn explain(
    evidence: &Evidence<'_>,
    query: &Query,
    options: &ExplainOptions<'_>,
) -> Result<Explanation, ExplainError> {
    query
        .validate()
        .map_err(|c| ExplainError::new(c, "query", "invalid query"))?;
    if matches!(query, Query::Diff) {
        return Err(diff_requires_pair());
    }
    if let Some(p) = options.redaction {
        p.validate()
            .map_err(|e| ExplainError::parse("redaction", e))?;
    }
    let evidence = evidence::Set::read(evidence)?;
    bounds::build(&evidence, query, options, bounds::Bounds::default())
}

fn dispatch(b: &mut facts::Builder<'_>, query: &Query) -> Result<Answer, ExplainError> {
    match query {
        Query::WhyOutcome => outcome::build(b),
        Query::TextOrigin => text::build(b),
        Query::Models => models::build(b),
        Query::WhySelected { item } => selected::build(b, item),
        Query::WhyExcluded {
            item: Item::String(item),
        } => excluded::build(b, item),
        Query::WhyOrder {
            a: Item::String(a),
            b: Item::String(c),
        } => order::build(b, a, c),
        Query::Diff => Err(diff_requires_pair()),
        // `WhySelected { item }` above takes every item kind, withheld ones
        // included, so only these two remain.
        Query::WhyExcluded { .. } | Query::WhyOrder { .. } => Err(ExplainError::new(
            RefusalCode::InvalidQuery,
            "query",
            "query items must be strings",
        )),
    }
}

fn diff_requires_pair() -> ExplainError {
    ExplainError::new(
        RefusalCode::InvalidQuery,
        "query",
        "use explain_diff with two evidence sets",
    )
}