1mod detector;
10mod merge;
11mod resolver;
12
13pub use detector::{ConflictDetector, ConflictInfo, ConflictType};
14pub use merge::{MergeResult, ThreeWayMerge};
15pub use resolver::{ConflictQueue, ConflictResolver, Resolution, ResolutionStrategy};
16
17use crate::types::Memory;
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SyncMemoryVersion {
24 pub memory: Memory,
26 pub version_id: String,
28 pub created_at: DateTime<Utc>,
30 pub source: String,
32 pub content_hash: String,
34}
35
36impl SyncMemoryVersion {
37 pub fn new(memory: Memory, source: impl Into<String>) -> Self {
39 let source_str = source.into();
40 let content_hash = Self::compute_hash(&memory);
41 Self {
42 memory,
43 version_id: format!("{}_{}", source_str, Utc::now().timestamp_millis()),
44 created_at: Utc::now(),
45 source: source_str,
46 content_hash,
47 }
48 }
49
50 fn compute_hash(memory: &Memory) -> String {
52 use sha2::{Digest, Sha256};
53 let mut hasher = Sha256::new();
54 hasher.update(memory.content.as_bytes());
55 hasher.update(
56 serde_json::to_string(&memory.metadata)
57 .unwrap_or_default()
58 .as_bytes(),
59 );
60 hex::encode(hasher.finalize())[..16].to_string()
61 }
62
63 pub fn has_same_content(&self, other: &SyncMemoryVersion) -> bool {
65 self.content_hash == other.content_hash
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Conflict {
72 pub id: String,
74 pub memory_id: i64,
76 pub base: Option<SyncMemoryVersion>,
78 pub local: SyncMemoryVersion,
80 pub remote: SyncMemoryVersion,
82 pub conflict_type: ConflictType,
84 pub detected_at: DateTime<Utc>,
86 pub resolved: bool,
88 pub resolution: Option<Resolution>,
90}
91
92impl Conflict {
93 pub fn new(
95 memory_id: i64,
96 base: Option<SyncMemoryVersion>,
97 local: SyncMemoryVersion,
98 remote: SyncMemoryVersion,
99 conflict_type: ConflictType,
100 ) -> Self {
101 Self {
102 id: uuid::Uuid::new_v4().to_string(),
103 memory_id,
104 base,
105 local,
106 remote,
107 conflict_type,
108 detected_at: Utc::now(),
109 resolved: false,
110 resolution: None,
111 }
112 }
113
114 pub fn can_auto_resolve(&self) -> bool {
116 matches!(
117 self.conflict_type,
118 ConflictType::MetadataOnly | ConflictType::TagsOnly | ConflictType::NonOverlapping
119 )
120 }
121
122 pub fn resolve(&mut self, resolution: Resolution) {
124 self.resolved = true;
125 self.resolution = Some(resolution);
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::types::MemoryType;
133 use std::collections::HashMap;
134
135 fn create_test_memory(content: &str) -> Memory {
136 Memory {
137 id: 1,
138 content: content.to_string(),
139 memory_type: MemoryType::Note,
140 tags: vec!["test".to_string()],
141 metadata: HashMap::new(),
142 importance: 0.5,
143 access_count: 0,
144 created_at: Utc::now(),
145 updated_at: Utc::now(),
146 last_accessed_at: None,
147 owner_id: None,
148 visibility: crate::types::Visibility::Private,
149 scope: crate::types::MemoryScope::Global,
150 workspace: "default".to_string(),
151 tier: crate::types::MemoryTier::Permanent,
152 version: 1,
153 has_embedding: false,
154 expires_at: None,
155 content_hash: None,
156 event_time: None,
157 event_duration_seconds: None,
158 trigger_pattern: None,
159 procedure_success_count: 0,
160 procedure_failure_count: 0,
161 summary_of_id: None,
162 lifecycle_state: crate::types::LifecycleState::Active,
163 }
164 }
165
166 #[test]
167 fn test_memory_version_hash() {
168 let memory = create_test_memory("Test content");
169 let v1 = SyncMemoryVersion::new(memory.clone(), "device1");
170 let v2 = SyncMemoryVersion::new(memory, "device2");
171
172 assert!(v1.has_same_content(&v2));
174 }
175
176 #[test]
177 fn test_different_content_different_hash() {
178 let m1 = create_test_memory("Content A");
179 let m2 = create_test_memory("Content B");
180
181 let v1 = SyncMemoryVersion::new(m1, "device1");
182 let v2 = SyncMemoryVersion::new(m2, "device1");
183
184 assert!(!v1.has_same_content(&v2));
185 }
186
187 #[test]
188 fn test_conflict_creation() {
189 let base = SyncMemoryVersion::new(create_test_memory("Original"), "base");
190 let local = SyncMemoryVersion::new(create_test_memory("Local change"), "local");
191 let remote = SyncMemoryVersion::new(create_test_memory("Remote change"), "remote");
192
193 let conflict = Conflict::new(1, Some(base), local, remote, ConflictType::ContentConflict);
194
195 assert!(!conflict.resolved);
196 assert!(conflict.resolution.is_none());
197 assert!(!conflict.can_auto_resolve());
198 }
199
200 #[test]
201 fn test_auto_resolvable_conflict() {
202 let local = SyncMemoryVersion::new(create_test_memory("Same"), "local");
203 let remote = SyncMemoryVersion::new(create_test_memory("Same"), "remote");
204
205 let conflict = Conflict::new(1, None, local, remote, ConflictType::MetadataOnly);
206
207 assert!(conflict.can_auto_resolve());
208 }
209}