Skip to main content

zeph_memory/
recall_view.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! View-aware recall types for `MemCoT` graph retrieval (issues #3574 / #3575).
5//!
6//! [`RecallView`] selects the enrichment level applied to the raw graph recall results.
7//! [`RecalledFact`] is the unified fact type returned by [`crate::semantic::SemanticMemory::recall_graph_view`].
8
9use crate::graph::types::GraphFact;
10use crate::types::MessageId;
11
12pub use zeph_common::memory::RecallView;
13
14/// A graph fact returned by the view-aware recall path.
15///
16/// Wraps a base [`GraphFact`] and carries optional enrichment fields populated
17/// depending on the [`RecallView`] in use.
18///
19/// # Examples
20///
21/// ```
22/// use zeph_memory::{RecalledFact, RecallView};
23/// use zeph_memory::graph::types::GraphFact;
24/// use zeph_memory::graph::EdgeType;
25///
26/// let base = GraphFact {
27///     entity_name: "Rust".to_string(),
28///     relation: "uses".to_string(),
29///     target_name: "LLVM".to_string(),
30///     fact: "Rust uses LLVM for code generation".to_string(),
31///     entity_match_score: 0.9,
32///     hop_distance: 0,
33///     confidence: 0.95,
34///     valid_from: None,
35///     edge_type: EdgeType::Semantic,
36///     retrieval_count: 1,
37///     edge_id: None,
38/// };
39/// let recalled = RecalledFact::from_graph_fact(base);
40/// assert!(recalled.activation_score.is_none());
41/// assert!(recalled.provenance_message_id.is_none());
42/// assert!(recalled.neighbors.is_empty());
43/// ```
44#[derive(Debug, Clone)]
45pub struct RecalledFact {
46    /// The base graph fact (text, relation, confidence, `edge_type`, `hop_distance`).
47    pub fact: GraphFact,
48    /// Spreading-activation score. `Some` when the SA path was used; `None` for plain BFS.
49    ///
50    /// Used by the assembler to render `(activation: X)` suffix and preserve
51    /// current output bytes when `view = Head`.
52    pub activation_score: Option<f32>,
53    /// Source-message ID for the edge (Zoom-In only).
54    pub provenance_message_id: Option<MessageId>,
55    /// ≤200-char snippet from the source message, with `\n\r<>` scrubbed (Zoom-In only).
56    pub provenance_snippet: Option<String>,
57    /// 1-hop neighbor facts deduped against the head set (Zoom-Out only).
58    pub neighbors: Vec<GraphFact>,
59}
60
61impl RecalledFact {
62    /// Wrap a plain `GraphFact` with no enrichment fields set.
63    #[must_use]
64    pub fn from_graph_fact(fact: GraphFact) -> Self {
65        Self {
66            fact,
67            activation_score: None,
68            provenance_message_id: None,
69            provenance_snippet: None,
70            neighbors: Vec::new(),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::graph::types::{EdgeType, GraphFact};
79    use crate::types::MessageId;
80
81    fn make_fact() -> GraphFact {
82        GraphFact {
83            entity_name: "Rust".to_string(),
84            relation: "uses".to_string(),
85            target_name: "LLVM".to_string(),
86            fact: "Rust uses LLVM".to_string(),
87            entity_match_score: 0.9,
88            hop_distance: 0,
89            confidence: 0.95,
90            valid_from: None,
91            edge_type: EdgeType::Semantic,
92            retrieval_count: 0,
93            edge_id: None,
94        }
95    }
96
97    #[test]
98    fn from_graph_fact_no_enrichment() {
99        let rf = RecalledFact::from_graph_fact(make_fact());
100        assert!(rf.activation_score.is_none());
101        assert!(rf.provenance_message_id.is_none());
102        assert!(rf.provenance_snippet.is_none());
103        assert!(rf.neighbors.is_empty());
104    }
105
106    #[test]
107    fn recall_view_default_is_head() {
108        assert_eq!(RecallView::default(), RecallView::Head);
109    }
110
111    // ── Snapshot tests: Head / ZoomIn / ZoomOut output shape ─────────────────
112
113    fn head_fact() -> RecalledFact {
114        RecalledFact::from_graph_fact(GraphFact {
115            entity_name: "Rust".to_string(),
116            relation: "uses".to_string(),
117            target_name: "LLVM".to_string(),
118            fact: "Rust uses LLVM for code generation".to_string(),
119            entity_match_score: 0.9,
120            hop_distance: 0,
121            confidence: 0.95,
122            valid_from: Some("2026-01-01".to_string()),
123            edge_type: EdgeType::Semantic,
124            retrieval_count: 1,
125            edge_id: Some(10),
126        })
127    }
128
129    #[test]
130    fn snapshot_head_no_enrichment() {
131        let rf = head_fact();
132        insta::assert_debug_snapshot!("head_view", rf);
133    }
134
135    #[test]
136    fn snapshot_zoom_in_with_provenance() {
137        let mut rf = head_fact();
138        rf.provenance_message_id = Some(MessageId(42));
139        rf.provenance_snippet = Some("The Rust compiler uses LLVM as its backend".to_string());
140        insta::assert_debug_snapshot!("zoom_in_view", rf);
141    }
142
143    #[test]
144    fn snapshot_zoom_out_with_neighbors() {
145        let mut rf = head_fact();
146        rf.neighbors.push(GraphFact {
147            entity_name: "LLVM".to_string(),
148            relation: "supports".to_string(),
149            target_name: "WebAssembly".to_string(),
150            fact: "LLVM supports WebAssembly output".to_string(),
151            entity_match_score: 0.5,
152            hop_distance: 1,
153            confidence: 0.8,
154            valid_from: None,
155            edge_type: EdgeType::Semantic,
156            retrieval_count: 0,
157            edge_id: Some(11),
158        });
159        insta::assert_debug_snapshot!("zoom_out_view", rf);
160    }
161
162    #[test]
163    fn snapshot_sa_fact_with_activation_score() {
164        // Simulate an SA-path fact: activation_score is Some, entity names are empty.
165        let rf = RecalledFact {
166            fact: GraphFact {
167                entity_name: String::new(),
168                relation: "uses".to_string(),
169                target_name: String::new(),
170                fact: "Rust uses LLVM for compilation".to_string(),
171                entity_match_score: 0.82,
172                hop_distance: 0,
173                confidence: 0.9,
174                valid_from: Some("2026-01-01".to_string()),
175                edge_type: EdgeType::Semantic,
176                retrieval_count: 0,
177                edge_id: Some(55),
178            },
179            activation_score: Some(0.82),
180            provenance_message_id: None,
181            provenance_snippet: None,
182            neighbors: Vec::new(),
183        };
184        insta::assert_debug_snapshot!("sa_head_view", rf);
185    }
186}