Skip to main content

canon_core/
edge.rs

1//! Edge types for the semantic graph
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Types of relationships in the cognitive graph
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[repr(u8)]
9pub enum EdgeKind {
10    /// Document contains a chunk
11    DocToChunk = 0,
12
13    /// Chunk has an embedding
14    ChunkToEmbedding = 1,
15
16    /// Semantic link between chunks (e.g., similar content)
17    ChunkToChunk = 2,
18
19    /// Document has a summary
20    DocToSummary = 3,
21
22    /// Document references another document
23    DocToDoc = 4,
24}
25
26impl EdgeKind {
27    /// Convert from u8 representation
28    pub fn from_u8(value: u8) -> Option<Self> {
29        match value {
30            0 => Some(Self::DocToChunk),
31            1 => Some(Self::ChunkToEmbedding),
32            2 => Some(Self::ChunkToChunk),
33            3 => Some(Self::DocToSummary),
34            4 => Some(Self::DocToDoc),
35            _ => None,
36        }
37    }
38}
39
40/// An edge in the semantic graph
41///
42/// Represents a directed relationship between two nodes.
43/// Per CP-001 §2.5: edges connect nodes with typed relationships.
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct Edge {
46    /// Source node ID
47    pub source: Uuid,
48
49    /// Target node ID
50    pub target: Uuid,
51
52    /// Type of relationship
53    pub kind: EdgeKind,
54
55    /// Optional weight/score (e.g., similarity score for `ChunkToChunk`)
56    /// Per CP-001: stored as f32 in range [0.0, 1.0]
57    pub weight: Option<f32>,
58
59    /// Optional metadata string (e.g., relationship context)
60    pub metadata: Option<String>,
61}
62
63impl Edge {
64    /// Create a new edge
65    pub fn new(source: Uuid, target: Uuid, kind: EdgeKind) -> Self {
66        Self {
67            source,
68            target,
69            kind,
70            weight: None,
71            metadata: None,
72        }
73    }
74
75    /// Create a weighted edge (e.g., for similarity scores)
76    pub fn with_weight(source: Uuid, target: Uuid, kind: EdgeKind, weight: f32) -> Self {
77        Self {
78            source,
79            target,
80            kind,
81            weight: Some(weight),
82            metadata: None,
83        }
84    }
85
86    /// Create a doc-to-chunk edge
87    pub fn doc_to_chunk(doc_id: Uuid, chunk_id: Uuid) -> Self {
88        Self::new(doc_id, chunk_id, EdgeKind::DocToChunk)
89    }
90
91    /// Create a chunk-to-embedding edge
92    pub fn chunk_to_embedding(chunk_id: Uuid, embedding_id: Uuid) -> Self {
93        Self::new(chunk_id, embedding_id, EdgeKind::ChunkToEmbedding)
94    }
95}
96
97impl Eq for Edge {}
98
99impl std::hash::Hash for Edge {
100    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
101        self.source.hash(state);
102        self.target.hash(state);
103        self.kind.hash(state);
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_edge_creation() {
113        let src = Uuid::from_bytes([1u8; 16]);
114        let tgt = Uuid::from_bytes([2u8; 16]);
115
116        let edge = Edge::doc_to_chunk(src, tgt);
117        assert_eq!(edge.source, src);
118        assert_eq!(edge.target, tgt);
119        assert_eq!(edge.kind, EdgeKind::DocToChunk);
120        assert!(edge.weight.is_none());
121        assert!(edge.metadata.is_none());
122    }
123
124    #[test]
125    fn test_weighted_edge() {
126        let edge = Edge::with_weight(
127            Uuid::from_bytes([1u8; 16]),
128            Uuid::from_bytes([2u8; 16]),
129            EdgeKind::ChunkToChunk,
130            0.85,
131        );
132
133        assert!((edge.weight.unwrap() - 0.85).abs() < 0.001);
134    }
135
136    #[test]
137    fn test_edge_kind_roundtrip() {
138        for kind in [
139            EdgeKind::DocToChunk,
140            EdgeKind::ChunkToEmbedding,
141            EdgeKind::ChunkToChunk,
142            EdgeKind::DocToSummary,
143            EdgeKind::DocToDoc,
144        ] {
145            let value = kind as u8;
146            assert_eq!(EdgeKind::from_u8(value), Some(kind));
147        }
148    }
149
150    #[test]
151    fn test_edge_with_metadata() {
152        let mut edge = Edge::new(
153            Uuid::from_bytes([1u8; 16]),
154            Uuid::from_bytes([2u8; 16]),
155            EdgeKind::ChunkToChunk,
156        );
157        edge.metadata = Some("related by topic".to_string());
158        assert_eq!(edge.metadata.as_deref(), Some("related by topic"));
159    }
160
161    // Additional tests for comprehensive coverage
162
163    #[test]
164    fn test_edge_new_doc_to_chunk() {
165        let doc_id = Uuid::from_bytes([1u8; 16]);
166        let chunk_id = Uuid::from_bytes([2u8; 16]);
167
168        let edge = Edge::doc_to_chunk(doc_id, chunk_id);
169
170        assert_eq!(edge.source, doc_id);
171        assert_eq!(edge.target, chunk_id);
172        assert_eq!(edge.kind, EdgeKind::DocToChunk);
173    }
174
175    #[test]
176    fn test_edge_new_chunk_to_embedding() {
177        let chunk_id = Uuid::from_bytes([1u8; 16]);
178        let embedding_id = Uuid::from_bytes([2u8; 16]);
179
180        let edge = Edge::chunk_to_embedding(chunk_id, embedding_id);
181
182        assert_eq!(edge.source, chunk_id);
183        assert_eq!(edge.target, embedding_id);
184        assert_eq!(edge.kind, EdgeKind::ChunkToEmbedding);
185    }
186
187    #[test]
188    fn test_edge_new_chunk_to_chunk() {
189        let chunk1 = Uuid::from_bytes([1u8; 16]);
190        let chunk2 = Uuid::from_bytes([2u8; 16]);
191
192        let edge = Edge::new(chunk1, chunk2, EdgeKind::ChunkToChunk);
193
194        assert_eq!(edge.source, chunk1);
195        assert_eq!(edge.target, chunk2);
196        assert_eq!(edge.kind, EdgeKind::ChunkToChunk);
197    }
198
199    #[test]
200    fn test_edge_with_weight() {
201        let edge = Edge::with_weight(
202            Uuid::from_bytes([1u8; 16]),
203            Uuid::from_bytes([2u8; 16]),
204            EdgeKind::ChunkToChunk,
205            0.95,
206        );
207
208        assert!(edge.weight.is_some());
209        assert!((edge.weight.unwrap() - 0.95).abs() < 0.001);
210    }
211
212    #[test]
213    fn test_edge_custom_kind() {
214        // EdgeKind doesn't have Custom(String), but we test all variants
215        let kinds = vec![
216            EdgeKind::DocToChunk,
217            EdgeKind::ChunkToEmbedding,
218            EdgeKind::ChunkToChunk,
219            EdgeKind::DocToSummary,
220            EdgeKind::DocToDoc,
221        ];
222
223        for kind in kinds {
224            let edge = Edge::new(
225                Uuid::from_bytes([1u8; 16]),
226                Uuid::from_bytes([2u8; 16]),
227                kind,
228            );
229            assert_eq!(edge.kind, kind);
230        }
231    }
232
233    #[test]
234    fn test_edge_canonical_bytes() {
235        let edge = Edge::new(
236            Uuid::from_bytes([1u8; 16]),
237            Uuid::from_bytes([2u8; 16]),
238            EdgeKind::DocToChunk,
239        );
240
241        // Verify all fields exist
242        assert_eq!(edge.source.as_bytes().len(), 16);
243        assert_eq!(edge.target.as_bytes().len(), 16);
244    }
245
246    #[test]
247    fn test_edge_uniqueness_constraint() {
248        // Test that edges with same source, target, and kind are equal
249        let edge1 = Edge::new(
250            Uuid::from_bytes([1u8; 16]),
251            Uuid::from_bytes([2u8; 16]),
252            EdgeKind::DocToChunk,
253        );
254        let edge2 = Edge::new(
255            Uuid::from_bytes([1u8; 16]),
256            Uuid::from_bytes([2u8; 16]),
257            EdgeKind::DocToChunk,
258        );
259        let edge3 = Edge::new(
260            Uuid::from_bytes([1u8; 16]),
261            Uuid::from_bytes([2u8; 16]),
262            EdgeKind::ChunkToChunk, // Different kind
263        );
264
265        // Same source, target, kind should be equal
266        assert_eq!(edge1, edge2);
267        // Different kind -> different edge
268        assert_ne!(edge1, edge3);
269    }
270
271    #[test]
272    fn test_edge_kind_serialization() {
273        // Test EdgeKind serialization/deserialization
274        for kind in [
275            EdgeKind::DocToChunk,
276            EdgeKind::ChunkToEmbedding,
277            EdgeKind::ChunkToChunk,
278            EdgeKind::DocToSummary,
279            EdgeKind::DocToDoc,
280        ] {
281            let value = kind as u8;
282            assert_eq!(EdgeKind::from_u8(value), Some(kind));
283        }
284
285        // Invalid value should return None
286        assert_eq!(EdgeKind::from_u8(255), None);
287    }
288
289    #[test]
290    fn test_edge_hash() {
291        use std::collections::HashSet;
292
293        let mut set = HashSet::new();
294        let edge1 = Edge::new(
295            Uuid::from_bytes([1u8; 16]),
296            Uuid::from_bytes([2u8; 16]),
297            EdgeKind::DocToChunk,
298        );
299        let edge2 = Edge::new(
300            Uuid::from_bytes([1u8; 16]),
301            Uuid::from_bytes([2u8; 16]),
302            EdgeKind::DocToChunk,
303        );
304
305        set.insert(edge1.clone());
306        assert!(set.contains(&edge2)); // Same edge should be in set
307    }
308}