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 }
}
}