use crate::{DocId, ExternalRef};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Evidence {
pub subject: DocId,
pub evidence: ExternalRef,
pub role: LinkRole,
pub captured_at_seq: u64,
pub immutable_hint: Option<String>,
}
impl Evidence {
#[must_use]
pub fn new(
subject: DocId,
evidence: ExternalRef,
role: LinkRole,
captured_at_seq: u64,
immutable_hint: Option<String>,
) -> Self {
Self {
subject,
evidence,
role,
captured_at_seq,
immutable_hint,
}
}
#[must_use]
pub fn predicate(&self) -> &'static str {
self.role.predicate()
}
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub enum LinkRole {
SourceDocument,
AccountingSupport,
ScheduleReference,
ProjectIssue,
PublishedTo,
}
impl LinkRole {
#[must_use]
pub fn predicate(self) -> &'static str {
match self {
Self::SourceDocument => "office/source-document",
Self::AccountingSupport => "office/accounting-support",
Self::ScheduleReference => "office/schedule-reference",
Self::ProjectIssue => "office/project-issue",
Self::PublishedTo => "office/published-to",
}
}
#[must_use]
pub fn from_predicate(predicate: &str) -> Option<Self> {
match predicate {
"office/source-document" => Some(Self::SourceDocument),
"office/accounting-support" => Some(Self::AccountingSupport),
"office/schedule-reference" => Some(Self::ScheduleReference),
"office/project-issue" => Some(Self::ProjectIssue),
"office/published-to" => Some(Self::PublishedTo),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evidence_json_round_trips_reference_only_fields() {
let evidence = Evidence::new(
DocId::new("task-17"),
ExternalRef::new(
"site/msgraph",
"messages/msg-1",
Some("etag-1".to_owned()),
Some("https://graph.example/messages/msg-1".to_owned()),
),
LinkRole::SourceDocument,
9,
Some("sha256:abc".to_owned()),
);
let encoded = serde_json::to_string(&evidence).unwrap();
let decoded: Evidence = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, evidence);
assert_eq!(decoded.predicate(), "office/source-document");
}
#[test]
fn ledger_voucher_uses_plain_external_ref() {
let evidence = Evidence::new(
DocId::new("annual-account-2026"),
ExternalRef::new("ledger", "voucher/2026/0007", None, None),
LinkRole::AccountingSupport,
42,
Some("voucher-digest".to_owned()),
);
assert_eq!(evidence.evidence.backend, "ledger");
assert_eq!(evidence.predicate(), "office/accounting-support");
}
}