use super::SyncMemoryVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictType {
ContentConflict,
MetadataOnly,
TagsOnly,
NonOverlapping,
DeleteModify,
CreateCreate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictInfo {
pub conflict_type: ConflictType,
pub severity: u8,
pub description: String,
pub suggested_strategy: String,
}
pub struct ConflictDetector {
overlap_threshold: f32,
}
impl Default for ConflictDetector {
fn default() -> Self {
Self::new()
}
}
impl ConflictDetector {
pub fn new() -> Self {
Self {
overlap_threshold: 0.3,
}
}
pub fn with_overlap_threshold(mut self, threshold: f32) -> Self {
self.overlap_threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn detect(
&self,
base: Option<&SyncMemoryVersion>,
local: &SyncMemoryVersion,
remote: &SyncMemoryVersion,
) -> Option<ConflictInfo> {
if local.has_same_content(remote) {
return None;
}
let content_differs = local.memory.content != remote.memory.content;
let tags_differ = local.memory.tags != remote.memory.tags;
let metadata_differs = local.memory.metadata != remote.memory.metadata;
let conflict_type = if !content_differs && !tags_differ && metadata_differs {
ConflictType::MetadataOnly
} else if !content_differs && tags_differ && !metadata_differs {
ConflictType::TagsOnly
} else if content_differs {
if let Some(base) = base {
if self.are_changes_overlapping(
&base.memory.content,
&local.memory.content,
&remote.memory.content,
) {
ConflictType::ContentConflict
} else {
ConflictType::NonOverlapping
}
} else {
ConflictType::ContentConflict
}
} else {
ConflictType::MetadataOnly
};
let (severity, description, suggested_strategy) = match conflict_type {
ConflictType::ContentConflict => (
80,
"Both local and remote modified the content in overlapping sections".to_string(),
"manual_review".to_string(),
),
ConflictType::MetadataOnly => (
20,
"Only metadata differs between versions".to_string(),
"merge_metadata".to_string(),
),
ConflictType::TagsOnly => (
10,
"Only tags differ between versions".to_string(),
"union_tags".to_string(),
),
ConflictType::NonOverlapping => (
40,
"Changes are in different sections and can be merged".to_string(),
"three_way_merge".to_string(),
),
ConflictType::DeleteModify => (
90,
"One side deleted the memory while the other modified it".to_string(),
"manual_review".to_string(),
),
ConflictType::CreateCreate => (
70,
"Both sides created a memory with the same ID".to_string(),
"keep_both".to_string(),
),
};
Some(ConflictInfo {
conflict_type,
severity,
description,
suggested_strategy,
})
}
fn are_changes_overlapping(&self, base: &str, local: &str, remote: &str) -> bool {
let base_lines: Vec<&str> = base.lines().collect();
let local_lines: Vec<&str> = local.lines().collect();
let remote_lines: Vec<&str> = remote.lines().collect();
let local_changes: Vec<usize> = local_lines
.iter()
.enumerate()
.filter_map(|(i, line)| {
if i >= base_lines.len() || base_lines[i] != *line {
Some(i)
} else {
None
}
})
.collect();
let remote_changes: Vec<usize> = remote_lines
.iter()
.enumerate()
.filter_map(|(i, line)| {
if i >= base_lines.len() || base_lines[i] != *line {
Some(i)
} else {
None
}
})
.collect();
let overlap_count = local_changes
.iter()
.filter(|l| remote_changes.contains(l))
.count();
let total_changes = local_changes.len().max(remote_changes.len());
if total_changes == 0 {
return false;
}
(overlap_count as f32 / total_changes as f32) > self.overlap_threshold
}
pub fn detect_delete_modify(
&self,
local_exists: bool,
remote_exists: bool,
local_modified: bool,
remote_modified: bool,
) -> Option<ConflictType> {
match (local_exists, remote_exists, local_modified, remote_modified) {
(false, true, _, true) => Some(ConflictType::DeleteModify),
(true, false, true, _) => Some(ConflictType::DeleteModify),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Memory, MemoryType, Visibility};
use chrono::Utc;
use std::collections::HashMap;
fn create_memory(content: &str) -> Memory {
Memory {
id: 1,
content: content.to_string(),
memory_type: MemoryType::Note,
tags: vec!["test".to_string()],
metadata: HashMap::new(),
importance: 0.5,
access_count: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
last_accessed_at: None,
owner_id: None,
visibility: Visibility::Private,
scope: crate::types::MemoryScope::Global,
workspace: "default".to_string(),
tier: crate::types::MemoryTier::Permanent,
version: 1,
has_embedding: false,
expires_at: None,
content_hash: None,
event_time: None,
event_duration_seconds: None,
trigger_pattern: None,
procedure_success_count: 0,
procedure_failure_count: 0,
summary_of_id: None,
lifecycle_state: crate::types::LifecycleState::Active,
}
}
fn create_version(content: &str, source: &str) -> SyncMemoryVersion {
SyncMemoryVersion::new(create_memory(content), source)
}
#[test]
fn test_no_conflict_same_content() {
let detector = ConflictDetector::new();
let local = create_version("Same content", "local");
let remote = create_version("Same content", "remote");
let conflict = detector.detect(None, &local, &remote);
assert!(conflict.is_none());
}
#[test]
fn test_content_conflict() {
let detector = ConflictDetector::new();
let base = create_version("Original line 1\nOriginal line 2", "base");
let local = create_version("Modified line 1\nOriginal line 2", "local");
let remote = create_version("Different line 1\nOriginal line 2", "remote");
let conflict = detector.detect(Some(&base), &local, &remote);
assert!(conflict.is_some());
let info = conflict.unwrap();
assert_eq!(info.conflict_type, ConflictType::ContentConflict);
assert!(info.severity >= 70);
}
#[test]
fn test_non_overlapping_changes() {
let detector = ConflictDetector::new();
let base = create_version("Line 1\nLine 2\nLine 3\nLine 4", "base");
let local = create_version("Modified 1\nLine 2\nLine 3\nLine 4", "local");
let remote = create_version("Line 1\nLine 2\nLine 3\nModified 4", "remote");
let conflict = detector.detect(Some(&base), &local, &remote);
assert!(conflict.is_some());
let info = conflict.unwrap();
assert_eq!(info.conflict_type, ConflictType::NonOverlapping);
}
#[test]
fn test_metadata_only_conflict() {
let detector = ConflictDetector::new();
let mut local_mem = create_memory("Same content");
local_mem
.metadata
.insert("key".to_string(), serde_json::json!("local_value"));
let mut remote_mem = create_memory("Same content");
remote_mem
.metadata
.insert("key".to_string(), serde_json::json!("remote_value"));
let local = SyncMemoryVersion::new(local_mem, "local");
let remote = SyncMemoryVersion::new(remote_mem, "remote");
let conflict = detector.detect(None, &local, &remote);
assert!(conflict.is_some());
let info = conflict.unwrap();
assert_eq!(info.conflict_type, ConflictType::MetadataOnly);
assert!(info.severity < 50);
}
#[test]
fn test_delete_modify_detection() {
let detector = ConflictDetector::new();
let result = detector.detect_delete_modify(false, true, false, true);
assert_eq!(result, Some(ConflictType::DeleteModify));
let result = detector.detect_delete_modify(true, false, true, false);
assert_eq!(result, Some(ConflictType::DeleteModify));
let result = detector.detect_delete_modify(true, true, true, true);
assert!(result.is_none());
}
}