use std::collections::HashSet;
use crate::agent::swarm::knowledge::{FileModification, SharedKnowledge};
#[derive(Debug, Clone)]
pub struct FileConflict {
pub path: String,
pub agents: Vec<String>,
pub resolution: Option<ConflictResolutionOutcome>,
}
#[derive(Debug, Clone)]
pub enum ConflictResolutionOutcome {
AutoMerged,
CoordinatorResolved {
explanation: String,
},
UserResolved { choice: String },
}
pub async fn detect_conflicts(knowledge: &SharedKnowledge) -> Vec<FileConflict> {
let conflicting = knowledge.conflicting_files().await;
conflicting
.into_iter()
.map(|(path, mods)| {
let agents: Vec<String> = mods
.iter()
.map(|m| m.agent_id.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect();
FileConflict {
path,
agents,
resolution: None,
}
})
.collect()
}
pub fn can_auto_merge(mod_a: &FileModification, mod_b: &FileModification) -> bool {
use super::knowledge::ModificationType;
match (&mod_a.modification_type, &mod_b.modification_type) {
(ModificationType::Created, _) | (_, ModificationType::Created) => false,
(ModificationType::Deleted, _) | (_, ModificationType::Deleted) => false,
(
ModificationType::Edited { new_hash: h1, .. },
ModificationType::Edited { old_hash: h2, .. },
) => h1 == h2,
}
}
pub async fn try_auto_merge_conflicts(
conflicts: &mut [FileConflict],
knowledge: &SharedKnowledge,
) -> usize {
let mut resolved = 0;
for conflict in conflicts.iter_mut() {
if conflict.resolution.is_some() {
continue;
}
let mods = knowledge.file_modifications(&conflict.path).await;
if mods.len() >= 2 {
let all_sequential = mods.windows(2).all(|w| can_auto_merge(&w[0], &w[1]));
if all_sequential {
conflict.resolution = Some(ConflictResolutionOutcome::AutoMerged);
resolved += 1;
let hashes: Vec<u64> = mods.iter().map(|m| m.content_hash).collect();
tracing::info!(
path = %conflict.path,
agents = ?conflict.agents,
content_hashes = ?hashes,
"Auto-merged conflict (sequential edits)"
);
}
}
}
resolved
}
pub fn conflicts_summary(conflicts: &[FileConflict]) -> Vec<(String, Vec<String>)> {
conflicts
.iter()
.map(|c| (c.path.clone(), c.agents.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::swarm::knowledge::ModificationType;
use std::time::Instant;
fn make_mod(agent_id: &str, mod_type: ModificationType) -> FileModification {
FileModification {
agent_id: agent_id.to_string(),
modification_type: mod_type,
content_hash: 0,
timestamp: Instant::now(),
content_snapshot: None,
}
}
#[test]
fn test_can_auto_merge_sequential() {
let a = make_mod(
"a1",
ModificationType::Edited {
old_hash: 100,
new_hash: 200,
},
);
let b = make_mod(
"a2",
ModificationType::Edited {
old_hash: 200,
new_hash: 300,
},
);
assert!(can_auto_merge(&a, &b));
}
#[test]
fn test_cannot_auto_merge_parallel() {
let a = make_mod(
"a1",
ModificationType::Edited {
old_hash: 100,
new_hash: 200,
},
);
let b = make_mod(
"a2",
ModificationType::Edited {
old_hash: 100,
new_hash: 300,
},
);
assert!(!can_auto_merge(&a, &b));
}
#[test]
fn test_cannot_auto_merge_create() {
let a = make_mod("a1", ModificationType::Created);
let b = make_mod(
"a2",
ModificationType::Edited {
old_hash: 0,
new_hash: 100,
},
);
assert!(!can_auto_merge(&a, &b));
}
#[tokio::test]
async fn test_detect_conflicts() {
let kb = SharedKnowledge::new();
kb.record_file_modification("a1", "src/lib.rs", ModificationType::Created, 100, None)
.await;
kb.record_file_modification("a2", "src/lib.rs", ModificationType::Created, 200, None)
.await;
kb.record_file_modification("a1", "src/main.rs", ModificationType::Created, 300, None)
.await;
let conflicts = detect_conflicts(&kb).await;
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].path, "src/lib.rs");
assert_eq!(conflicts[0].agents.len(), 2);
}
#[test]
fn test_conflicts_summary() {
let conflicts = vec![FileConflict {
path: "src/lib.rs".to_string(),
agents: vec!["a1".to_string(), "a2".to_string()],
resolution: None,
}];
let summary = conflicts_summary(&conflicts);
assert_eq!(summary.len(), 1);
assert_eq!(summary[0].0, "src/lib.rs");
}
#[test]
fn test_cannot_auto_merge_delete() {
let a = make_mod(
"a1",
ModificationType::Edited {
old_hash: 100,
new_hash: 200,
},
);
let b = make_mod("a2", ModificationType::Deleted);
assert!(!can_auto_merge(&a, &b));
}
#[test]
fn test_resolution_variants_debug() {
let user = ConflictResolutionOutcome::UserResolved {
choice: "keep-mine".to_string(),
};
let coord = ConflictResolutionOutcome::CoordinatorResolved {
explanation: "Merged edits".to_string(),
};
assert!(format!("{user:?}").contains("keep-mine"));
assert!(format!("{coord:?}").contains("Merged edits"));
}
}