Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/mimir-room/src/evidence.rs

//! The evidence record every answer carries — v1's `evidence_note` law:
//! never a silent empty. Which tables / streams fed the room, how many rows,
//! which edge kinds the query walked, and what is MISSING (no binary stream
//! for a repo, no edge of a kind, a seed the room does not know). An empty
//! answer with nothing missing says so in words — and in [`AnswerState`],
//! for the readers that are programs.
//!
//! Everything here is `serde`: an RPC / MCP front door carries the record as
//! it is (`kinds` as `{"kind": "CALLS", "edges": 8}`).

use std::borrow::Cow;
use std::fmt;

use mimir_schema::EdgeKind;
use serde::{Deserialize, Serialize};

/// One event table the room was folded from.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Input {
    /// `code_node_events`, `bin_edge_events`, …
    pub table: Cow<'static, str>,
    /// `source` or `binary`.
    pub stream: Cow<'static, str>,
    /// Events inside the scope the fold consumed (both ops).
    pub events_read: u64,
    /// Identities open after the fold.
    pub open_rows: u64,
}

/// Something the answer would need and the room does not have.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Missing {
    /// What is missing: a table, a stream, an edge kind, a seed.
    pub input: String,
    /// The repo it is missing for, when it is per repo.
    pub repo: Option<String>,
    /// Why, and what the answer therefore cannot say.
    pub why: String,
}

/// What an answer IS, for a reader that cannot read the notes.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnswerState {
    /// A value, and every input it needed was there.
    #[default]
    Complete,
    /// A value, but inputs are MISSING: a lower bound, not the whole answer.
    Partial,
    /// Empty with every input present: the room holds no match — a fact.
    EmptyFact,
    /// Empty AND inputs are missing: the empty set may be the gap.
    EmptyGap,
}

impl AnswerState {
    pub fn as_str(self) -> &'static str {
        match self {
            AnswerState::Complete => "complete",
            AnswerState::Partial => "partial",
            AnswerState::EmptyFact => "empty_fact",
            AnswerState::EmptyGap => "empty_gap",
        }
    }
}

/// The answer was cut at a caller-given cap.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capped {
    pub shown: u64,
    pub total: u64,
}

mod kinds_serde {
    use super::*;
    use serde::{Deserializer, Serializer, de::Error};

    #[derive(Serialize, Deserialize)]
    struct Walked {
        kind: String,
        edges: u64,
    }

    pub fn serialize<S: Serializer>(v: &[(EdgeKind, u64)], s: S) -> Result<S::Ok, S::Error> {
        s.collect_seq(v.iter().map(|(k, n)| Walked { kind: k.as_str().to_string(), edges: *n }))
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<(EdgeKind, u64)>, D::Error> {
        Vec::<Walked>::deserialize(d)?
            .into_iter()
            .map(|w| EdgeKind::parse(&w.kind).map(|k| (k, w.edges)).ok_or_else(|| D::Error::custom(format!("unknown edge kind `{}`", w.kind))))
            .collect()
    }
}

/// The evidence of one answer.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Evidence {
    /// The query, as called.
    pub query: String,
    /// The scope (repo@seq …, universe on/off).
    pub scope: String,
    /// Complete / partial / empty-fact / empty-gap (set when the answer is made).
    pub state: AnswerState,
    /// The tables that fed the room.
    pub inputs: Vec<Input>,
    /// Edge kinds the query walked, with their edge count in the room.
    #[serde(with = "kinds_serde")]
    pub kinds: Vec<(EdgeKind, u64)>,
    pub missing: Vec<Missing>,
    pub notes: Vec<String>,
    /// Set when a cap cut the answer (the rest exists and was not returned).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub capped: Option<Capped>,
}

impl Evidence {
    /// Nothing the answer needed was missing.
    pub fn is_complete(&self) -> bool {
        self.missing.is_empty()
    }

    pub(crate) fn miss(&mut self, input: impl Into<String>, repo: Option<&str>, why: impl Into<String>) {
        let m = Missing { input: input.into(), repo: repo.map(str::to_string), why: why.into() };
        if !self.missing.contains(&m) {
            self.missing.push(m);
        }
    }

    pub(crate) fn note(&mut self, n: impl Into<String>) {
        let n = n.into();
        if !self.notes.contains(&n) {
            self.notes.push(n);
        }
    }

    /// Record that `total` matched and only `shown` are returned.
    pub(crate) fn cap(&mut self, shown: usize, total: usize) {
        if shown < total {
            self.capped = Some(Capped { shown: shown as u64, total: total as u64 });
            self.note(format!("capped: {shown} of {total} returned"));
        }
    }

    /// Is something missing for `input` (exact) — test and caller helper.
    pub fn missing_input(&self, input: &str) -> Vec<&Missing> {
        self.missing.iter().filter(|m| m.input == input).collect()
    }
}

impl fmt::Display for Evidence {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "evidence: {}", self.query)?;
        writeln!(f, "  scope: {}", self.scope)?;
        writeln!(f, "  state: {}", self.state.as_str())?;
        for i in &self.inputs {
            writeln!(f, "  input {:<20} {:<6} events {:>10} open {:>10}", i.table, i.stream, i.events_read, i.open_rows)?;
        }
        if !self.kinds.is_empty() {
            let k: Vec<String> = self.kinds.iter().map(|(k, n)| format!("{k}={n}")).collect();
            writeln!(f, "  walked: {}", k.join(" "))?;
        }
        for m in &self.missing {
            match &m.repo {
                Some(r) => writeln!(f, "  MISSING {} [{r}]: {}", m.input, m.why)?,
                None => writeln!(f, "  MISSING {}: {}", m.input, m.why)?,
            }
        }
        for n in &self.notes {
            writeln!(f, "  note: {n}")?;
        }
        Ok(())
    }
}

/// An answer and the evidence it rests on.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Answer<T> {
    pub value: T,
    pub evidence: Evidence,
}

/// What "empty" means for an answer value.
pub trait Emptiness {
    fn is_empty_answer(&self) -> bool;
}

impl<T> Emptiness for Vec<T> {
    fn is_empty_answer(&self) -> bool {
        self.is_empty()
    }
}

impl<T> Emptiness for Option<T> {
    fn is_empty_answer(&self) -> bool {
        self.is_none()
    }
}

impl<T> Answer<T> {
    pub(crate) fn new(value: T, mut evidence: Evidence) -> Self
    where
        T: Emptiness,
    {
        let (empty, gaps) = (value.is_empty_answer(), !evidence.missing.is_empty());
        evidence.state = match (empty, gaps) {
            (false, false) => AnswerState::Complete,
            (false, true) => AnswerState::Partial,
            (true, false) => AnswerState::EmptyFact,
            (true, true) => AnswerState::EmptyGap,
        };
        if empty && !gaps {
            evidence.note("empty answer with every input present: the room holds no match (a fact, not a gap)");
        } else if empty {
            evidence.note("empty answer, and inputs are MISSING (above): the empty set may be the gap, not a fact");
        }
        Self { value, evidence }
    }

    /// The same evidence over a value derived from this one (state kept).
    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Answer<U> {
        Answer { value: f(self.value), evidence: self.evidence }
    }
}