Skip to main content

sim_lib_doc_core/
evidence.rs

1//! Cross-object evidence links between office documents and external records.
2
3use crate::{DocId, ExternalRef};
4
5/// A reference-only evidence link for a document subject.
6#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7pub struct Evidence {
8    /// Local document or task being supported by the external reference.
9    pub subject: DocId,
10    /// External item, file, message, voucher, task, or published target.
11    pub evidence: ExternalRef,
12    /// Relationship between the subject and the evidence object.
13    pub role: LinkRole,
14    /// Authoritative ledger or capture sequence at which the link was recorded.
15    pub captured_at_seq: u64,
16    /// Optional immutable marker such as an ETag, content hash, or voucher digest.
17    pub immutable_hint: Option<String>,
18}
19
20impl Evidence {
21    /// Builds an evidence link.
22    #[must_use]
23    pub fn new(
24        subject: DocId,
25        evidence: ExternalRef,
26        role: LinkRole,
27        captured_at_seq: u64,
28        immutable_hint: Option<String>,
29    ) -> Self {
30        Self {
31            subject,
32            evidence,
33            role,
34            captured_at_seq,
35            immutable_hint,
36        }
37    }
38
39    /// Returns the claim predicate used to store this link as a fact row.
40    #[must_use]
41    pub fn predicate(&self) -> &'static str {
42        self.role.predicate()
43    }
44}
45
46/// Role used as the predicate for a cross-object evidence fact.
47#[derive(
48    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
49)]
50pub enum LinkRole {
51    /// The external object is the source document for the subject.
52    SourceDocument,
53    /// The external object supports an accounting entry or statement.
54    AccountingSupport,
55    /// The external object anchors a schedule or Gantt task reference.
56    ScheduleReference,
57    /// The external object is a project issue or field item.
58    ProjectIssue,
59    /// The subject was published to the external object.
60    PublishedTo,
61}
62
63impl LinkRole {
64    /// Returns the stable claim predicate for this role.
65    #[must_use]
66    pub fn predicate(self) -> &'static str {
67        match self {
68            Self::SourceDocument => "office/source-document",
69            Self::AccountingSupport => "office/accounting-support",
70            Self::ScheduleReference => "office/schedule-reference",
71            Self::ProjectIssue => "office/project-issue",
72            Self::PublishedTo => "office/published-to",
73        }
74    }
75
76    /// Decodes a stable claim predicate into a link role.
77    #[must_use]
78    pub fn from_predicate(predicate: &str) -> Option<Self> {
79        match predicate {
80            "office/source-document" => Some(Self::SourceDocument),
81            "office/accounting-support" => Some(Self::AccountingSupport),
82            "office/schedule-reference" => Some(Self::ScheduleReference),
83            "office/project-issue" => Some(Self::ProjectIssue),
84            "office/published-to" => Some(Self::PublishedTo),
85            _ => None,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn evidence_json_round_trips_reference_only_fields() {
96        let evidence = Evidence::new(
97            DocId::new("task-17"),
98            ExternalRef::new(
99                "site/msgraph",
100                "messages/msg-1",
101                Some("etag-1".to_owned()),
102                Some("https://graph.example/messages/msg-1".to_owned()),
103            ),
104            LinkRole::SourceDocument,
105            9,
106            Some("sha256:abc".to_owned()),
107        );
108
109        let encoded = serde_json::to_string(&evidence).unwrap();
110        let decoded: Evidence = serde_json::from_str(&encoded).unwrap();
111
112        assert_eq!(decoded, evidence);
113        assert_eq!(decoded.predicate(), "office/source-document");
114    }
115
116    #[test]
117    fn ledger_voucher_uses_plain_external_ref() {
118        let evidence = Evidence::new(
119            DocId::new("annual-account-2026"),
120            ExternalRef::new("ledger", "voucher/2026/0007", None, None),
121            LinkRole::AccountingSupport,
122            42,
123            Some("voucher-digest".to_owned()),
124        );
125
126        assert_eq!(evidence.evidence.backend, "ledger");
127        assert_eq!(evidence.predicate(), "office/accounting-support");
128    }
129}