Skip to main content

rustev_eval/explain/
mod.rs

1//! Pure explanations of retained decision evidence (spec 040).
2mod bounds;
3mod diff;
4mod render;
5pub use diff::explain_diff;
6pub use render::render_markdown;
7mod evidence;
8mod excluded;
9mod facts;
10mod models;
11mod order;
12mod outcome;
13mod redact;
14mod scopes;
15mod selected;
16mod text;
17
18use crate::resolve::Resolver;
19use rustev_contract::explanation::*;
20use rustev_contract::redaction::RedactionPolicy;
21use rustev_contract::scope::Scope;
22use rustev_contract::time::Timestamp;
23
24pub enum Evidence<'a> {
25    Record {
26        record: &'a [u8],
27        plan: Option<&'a [u8]>,
28    },
29    Bundle {
30        bundle: &'a [u8],
31        scope: &'a Scope,
32        now_ms: Timestamp,
33        resolver: &'a dyn Resolver,
34    },
35}
36#[derive(Default)]
37pub struct ExplainOptions<'a> {
38    pub redaction: Option<&'a RedactionPolicy>,
39}
40#[derive(Debug)]
41pub struct ExplainError {
42    pub code: RefusalCode,
43    pub subject: String,
44    pub detail: String,
45}
46impl std::fmt::Display for ExplainError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(
49            f,
50            "{}: {}: {}",
51            self.code.as_str(),
52            self.subject,
53            self.detail
54        )
55    }
56}
57impl std::error::Error for ExplainError {}
58impl ExplainError {
59    fn new(code: RefusalCode, subject: &str, detail: impl ToString) -> Self {
60        Self {
61            code,
62            subject: subject.into(),
63            detail: detail.to_string(),
64        }
65    }
66    fn parse(subject: &str, e: impl ToString) -> Self {
67        Self::new(RefusalCode::Parse, subject, e)
68    }
69}
70pub fn explain(
71    evidence: &Evidence<'_>,
72    query: &Query,
73    options: &ExplainOptions<'_>,
74) -> Result<Explanation, ExplainError> {
75    query
76        .validate()
77        .map_err(|c| ExplainError::new(c, "query", "invalid query"))?;
78    if matches!(query, Query::Diff) {
79        return Err(diff_requires_pair());
80    }
81    if let Some(p) = options.redaction {
82        p.validate()
83            .map_err(|e| ExplainError::parse("redaction", e))?;
84    }
85    let evidence = evidence::Set::read(evidence)?;
86    bounds::build(&evidence, query, options, bounds::Bounds::default())
87}
88
89fn dispatch(b: &mut facts::Builder<'_>, query: &Query) -> Result<Answer, ExplainError> {
90    match query {
91        Query::WhyOutcome => outcome::build(b),
92        Query::TextOrigin => text::build(b),
93        Query::Models => models::build(b),
94        Query::WhySelected { item } => selected::build(b, item),
95        Query::WhyExcluded {
96            item: Item::String(item),
97        } => excluded::build(b, item),
98        Query::WhyOrder {
99            a: Item::String(a),
100            b: Item::String(c),
101        } => order::build(b, a, c),
102        Query::Diff => Err(diff_requires_pair()),
103        // `WhySelected { item }` above takes every item kind, withheld ones
104        // included, so only these two remain.
105        Query::WhyExcluded { .. } | Query::WhyOrder { .. } => Err(ExplainError::new(
106            RefusalCode::InvalidQuery,
107            "query",
108            "query items must be strings",
109        )),
110    }
111}
112
113fn diff_requires_pair() -> ExplainError {
114    ExplainError::new(
115        RefusalCode::InvalidQuery,
116        "query",
117        "use explain_diff with two evidence sets",
118    )
119}