use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use crate::communication::{ConflictInfo, ConflictType};
use crate::file_locks::{FileLockManager, LockType};
use crate::operation_tracker::OperationTracker;
use crate::resource_locks::{ResourceLockManager, ResourceScope, ResourceType};
#[derive(Debug, Clone)]
pub enum ConflictCheck {
Clear,
Blocked(Vec<Conflict>),
Warning(Vec<Conflict>),
}
impl ConflictCheck {
pub fn can_proceed(&self) -> bool {
matches!(self, ConflictCheck::Clear | ConflictCheck::Warning(_))
}
pub fn is_blocked(&self) -> bool {
matches!(self, ConflictCheck::Blocked(_))
}
pub fn conflicts(&self) -> &[Conflict] {
match self {
ConflictCheck::Clear => &[],
ConflictCheck::Blocked(c) | ConflictCheck::Warning(c) => c,
}
}
}
#[derive(Debug, Clone)]
pub struct Conflict {
pub conflict_type: ResourceConflictType,
pub holder_agent: String,
pub resource: String,
pub started_at: Instant,
pub status: String,
pub description: String,
}
impl Conflict {
pub fn to_conflict_info(&self) -> ConflictInfo {
ConflictInfo {
conflict_type: match &self.conflict_type {
ResourceConflictType::FileWriteBlocksBuild { path } => {
ConflictType::FileWriteBlocksBuild { path: path.clone() }
}
ResourceConflictType::BuildBlocksFileWrite => ConflictType::BuildBlocksFileWrite,
ResourceConflictType::TestBlocksFileWrite => ConflictType::TestBlocksFileWrite,
ResourceConflictType::GitBlocksFileWrite => ConflictType::GitBlocksFileWrite,
ResourceConflictType::FileWriteBlocksGit { path } => {
ConflictType::FileWriteBlocksGit { path: path.clone() }
}
ResourceConflictType::BuildBlocksGit => ConflictType::BuildBlocksGit,
},
holder_agent: self.holder_agent.clone(),
resource: self.resource.clone(),
duration_secs: self.started_at.elapsed().as_secs(),
status: self.status.clone(),
}
}
}
#[derive(Debug, Clone)]
pub enum ResourceConflictType {
FileWriteBlocksBuild {
path: PathBuf,
},
BuildBlocksFileWrite,
TestBlocksFileWrite,
GitBlocksFileWrite,
FileWriteBlocksGit {
path: PathBuf,
},
BuildBlocksGit,
}
#[derive(Debug, Clone)]
pub enum ProposedOperation {
FileWrite {
path: PathBuf,
agent_id: String,
},
Build {
scope: ResourceScope,
agent_id: String,
},
Test {
scope: ResourceScope,
agent_id: String,
},
GitStaging {
scope: ResourceScope,
agent_id: String,
},
GitCommit {
scope: ResourceScope,
agent_id: String,
},
GitPush {
scope: ResourceScope,
agent_id: String,
},
GitPull {
scope: ResourceScope,
agent_id: String,
},
}
impl ProposedOperation {
pub fn agent_id(&self) -> &str {
match self {
ProposedOperation::FileWrite { agent_id, .. }
| ProposedOperation::Build { agent_id, .. }
| ProposedOperation::Test { agent_id, .. }
| ProposedOperation::GitStaging { agent_id, .. }
| ProposedOperation::GitCommit { agent_id, .. }
| ProposedOperation::GitPush { agent_id, .. }
| ProposedOperation::GitPull { agent_id, .. } => agent_id,
}
}
}
pub struct ResourceChecker {
file_locks: Arc<FileLockManager>,
resource_locks: Arc<ResourceLockManager>,
_operation_tracker: Option<Arc<OperationTracker>>,
source_patterns: Vec<String>,
}
impl ResourceChecker {
pub fn new(file_locks: Arc<FileLockManager>, resource_locks: Arc<ResourceLockManager>) -> Self {
Self {
file_locks,
resource_locks,
_operation_tracker: None,
source_patterns: default_source_patterns(),
}
}
pub fn with_operation_tracker(
file_locks: Arc<FileLockManager>,
resource_locks: Arc<ResourceLockManager>,
operation_tracker: Arc<OperationTracker>,
) -> Self {
Self {
file_locks,
resource_locks,
_operation_tracker: Some(operation_tracker),
source_patterns: default_source_patterns(),
}
}
pub fn with_source_patterns(mut self, patterns: Vec<String>) -> Self {
self.source_patterns = patterns;
self
}
pub async fn can_start_build(&self, scope: &ResourceScope, agent_id: &str) -> ConflictCheck {
let file_locks = self.file_locks.list_locks().await;
let mut conflicts = Vec::new();
for (path, lock_info) in file_locks {
if lock_info.agent_id == agent_id {
continue;
}
if lock_info.lock_type != LockType::Write {
continue;
}
if self.is_in_scope(&path, scope) && self.is_source_file(&path) {
conflicts.push(Conflict {
conflict_type: ResourceConflictType::FileWriteBlocksBuild {
path: path.clone(),
},
holder_agent: lock_info.agent_id.clone(),
resource: path.display().to_string(),
started_at: lock_info.acquired_at,
status: "File locked for editing".to_string(),
description: format!("Write lock on {}", path.display()),
});
}
}
if conflicts.is_empty() {
ConflictCheck::Clear
} else {
ConflictCheck::Blocked(conflicts)
}
}
pub async fn can_write_file(&self, path: &Path, agent_id: &str) -> ConflictCheck {
let resource_locks = self.resource_locks.list_locks().await;
let mut conflicts = Vec::new();
let file_scope = self.scope_for_path(path);
for lock_info in resource_locks {
if lock_info.agent_id == agent_id {
continue;
}
if !self.scopes_overlap(&lock_info.scope, &file_scope) {
continue;
}
if !self.is_source_file(path) {
continue;
}
match lock_info.resource_type {
ResourceType::Build | ResourceType::BuildTest => {
conflicts.push(Conflict {
conflict_type: ResourceConflictType::BuildBlocksFileWrite,
holder_agent: lock_info.agent_id.clone(),
resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
started_at: lock_info.acquired_at,
status: lock_info.status.clone(),
description: lock_info.description.clone(),
});
}
ResourceType::Test => {
conflicts.push(Conflict {
conflict_type: ResourceConflictType::TestBlocksFileWrite,
holder_agent: lock_info.agent_id.clone(),
resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
started_at: lock_info.acquired_at,
status: lock_info.status.clone(),
description: lock_info.description.clone(),
});
}
ResourceType::GitIndex
| ResourceType::GitCommit
| ResourceType::GitRemoteWrite
| ResourceType::GitRemoteMerge
| ResourceType::GitBranch
| ResourceType::GitDestructive => {
if lock_info.resource_type == ResourceType::GitRemoteMerge
|| lock_info.resource_type == ResourceType::GitDestructive
{
conflicts.push(Conflict {
conflict_type: ResourceConflictType::GitBlocksFileWrite,
holder_agent: lock_info.agent_id.clone(),
resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
started_at: lock_info.acquired_at,
status: lock_info.status.clone(),
description: lock_info.description.clone(),
});
}
}
}
}
if conflicts.is_empty() {
ConflictCheck::Clear
} else {
ConflictCheck::Blocked(conflicts)
}
}
pub async fn can_start_git_operation(
&self,
git_op: ResourceType,
scope: &ResourceScope,
agent_id: &str,
) -> ConflictCheck {
let mut conflicts = Vec::new();
if matches!(
git_op,
ResourceType::GitIndex | ResourceType::GitCommit | ResourceType::GitRemoteWrite
) {
let file_locks = self.file_locks.list_locks().await;
for (path, lock_info) in file_locks {
if lock_info.agent_id == agent_id {
continue;
}
if lock_info.lock_type != LockType::Write {
continue;
}
if self.is_in_scope(&path, scope) && self.is_source_file(&path) {
conflicts.push(Conflict {
conflict_type: ResourceConflictType::FileWriteBlocksGit {
path: path.clone(),
},
holder_agent: lock_info.agent_id.clone(),
resource: path.display().to_string(),
started_at: lock_info.acquired_at,
status: "File locked for editing".to_string(),
description: format!("Write lock on {}", path.display()),
});
}
}
}
if matches!(
git_op,
ResourceType::GitCommit | ResourceType::GitRemoteWrite
) {
let resource_locks = self.resource_locks.list_locks().await;
for lock_info in resource_locks {
if lock_info.agent_id == agent_id {
continue;
}
if !self.scopes_overlap(&lock_info.scope, scope) {
continue;
}
if matches!(
lock_info.resource_type,
ResourceType::Build | ResourceType::Test | ResourceType::BuildTest
) {
conflicts.push(Conflict {
conflict_type: ResourceConflictType::BuildBlocksGit,
holder_agent: lock_info.agent_id.clone(),
resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
started_at: lock_info.acquired_at,
status: lock_info.status.clone(),
description: lock_info.description.clone(),
});
}
}
}
if conflicts.is_empty() {
ConflictCheck::Clear
} else {
ConflictCheck::Blocked(conflicts)
}
}
pub async fn check_conflicts(&self, operation: &ProposedOperation) -> ConflictCheck {
match operation {
ProposedOperation::FileWrite { path, agent_id } => {
self.can_write_file(path, agent_id).await
}
ProposedOperation::Build { scope, agent_id } => {
self.can_start_build(scope, agent_id).await
}
ProposedOperation::Test { scope, agent_id } => {
self.can_start_build(scope, agent_id).await
}
ProposedOperation::GitStaging { scope, agent_id } => {
self.can_start_git_operation(ResourceType::GitIndex, scope, agent_id)
.await
}
ProposedOperation::GitCommit { scope, agent_id } => {
self.can_start_git_operation(ResourceType::GitCommit, scope, agent_id)
.await
}
ProposedOperation::GitPush { scope, agent_id } => {
self.can_start_git_operation(ResourceType::GitRemoteWrite, scope, agent_id)
.await
}
ProposedOperation::GitPull { scope, agent_id } => {
self.can_start_git_operation(ResourceType::GitRemoteMerge, scope, agent_id)
.await
}
}
}
pub async fn get_build_blockers(&self, scope: &ResourceScope, agent_id: &str) -> Vec<Conflict> {
match self.can_start_build(scope, agent_id).await {
ConflictCheck::Blocked(conflicts) => conflicts,
_ => Vec::new(),
}
}
pub async fn get_file_write_blockers(&self, path: &Path, agent_id: &str) -> Vec<Conflict> {
match self.can_write_file(path, agent_id).await {
ConflictCheck::Blocked(conflicts) => conflicts,
_ => Vec::new(),
}
}
fn is_in_scope(&self, path: &Path, scope: &ResourceScope) -> bool {
match scope {
ResourceScope::Global => true,
ResourceScope::Project(project_path) => path.starts_with(project_path),
}
}
fn scope_for_path(&self, path: &Path) -> ResourceScope {
let mut current = path.parent();
while let Some(dir) = current {
if dir.join("Cargo.toml").exists()
|| dir.join("package.json").exists()
|| dir.join(".git").exists()
{
return ResourceScope::Project(dir.to_path_buf());
}
current = dir.parent();
}
ResourceScope::Global
}
fn scopes_overlap(&self, scope1: &ResourceScope, scope2: &ResourceScope) -> bool {
match (scope1, scope2) {
(ResourceScope::Global, _) | (_, ResourceScope::Global) => true,
(ResourceScope::Project(p1), ResourceScope::Project(p2)) => {
p1.starts_with(p2) || p2.starts_with(p1)
}
}
}
fn is_source_file(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.source_patterns {
if matches_pattern(&path_str, pattern) {
return true;
}
}
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
matches!(
ext.as_str(),
"rs" | "ts"
| "tsx"
| "js"
| "jsx"
| "py"
| "go"
| "java"
| "c"
| "cpp"
| "h"
| "hpp"
| "cs"
| "swift"
| "kt"
| "scala"
| "rb"
| "php"
)
} else {
false
}
}
}
fn default_source_patterns() -> Vec<String> {
vec![
"src/**/*".to_string(),
"lib/**/*".to_string(),
"crates/**/*".to_string(),
"packages/**/*".to_string(),
"app/**/*".to_string(),
]
}
fn matches_pattern(path: &str, pattern: &str) -> bool {
if pattern.contains("**") {
let parts: Vec<&str> = pattern.split("**").collect();
if parts.len() == 2 {
let prefix = parts[0].trim_end_matches('/');
let suffix = parts[1].trim_start_matches('/');
let has_prefix = prefix.is_empty() || path.starts_with(prefix);
let has_suffix = suffix.is_empty()
|| suffix == "*"
|| path.ends_with(suffix.trim_start_matches('*'));
return has_prefix && has_suffix;
}
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
return path.starts_with(parts[0]) && path.ends_with(parts[1]);
}
}
path == pattern
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matches_pattern() {
assert!(matches_pattern("src/main.rs", "src/**/*"));
assert!(matches_pattern("src/lib/utils.rs", "src/**/*"));
assert!(matches_pattern("crates/foo/src/lib.rs", "crates/**/*"));
assert!(!matches_pattern("target/debug/main", "src/**/*"));
}
#[test]
fn test_is_source_file() {
let checker = ResourceChecker::new(
Arc::new(FileLockManager::new()),
Arc::new(ResourceLockManager::new()),
);
assert!(checker.is_source_file(Path::new("src/main.rs")));
assert!(checker.is_source_file(Path::new("lib/index.ts")));
assert!(checker.is_source_file(Path::new("app.py")));
assert!(!checker.is_source_file(Path::new("README.md")));
assert!(!checker.is_source_file(Path::new("Cargo.toml")));
}
#[test]
fn test_scopes_overlap() {
let checker = ResourceChecker::new(
Arc::new(FileLockManager::new()),
Arc::new(ResourceLockManager::new()),
);
assert!(checker.scopes_overlap(&ResourceScope::Global, &ResourceScope::Global));
assert!(checker.scopes_overlap(
&ResourceScope::Global,
&ResourceScope::Project(PathBuf::from("/foo"))
));
assert!(checker.scopes_overlap(
&ResourceScope::Project(PathBuf::from("/foo")),
&ResourceScope::Project(PathBuf::from("/foo"))
));
assert!(checker.scopes_overlap(
&ResourceScope::Project(PathBuf::from("/foo")),
&ResourceScope::Project(PathBuf::from("/foo/bar"))
));
assert!(!checker.scopes_overlap(
&ResourceScope::Project(PathBuf::from("/foo")),
&ResourceScope::Project(PathBuf::from("/baz"))
));
}
#[tokio::test]
async fn test_can_start_build_no_conflicts() {
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
let checker = ResourceChecker::new(file_locks, resource_locks);
let scope = ResourceScope::Project(PathBuf::from("/test/project"));
let result = checker.can_start_build(&scope, "agent-1").await;
assert!(matches!(result, ConflictCheck::Clear));
}
#[tokio::test]
async fn test_can_write_file_no_conflicts() {
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
let checker = ResourceChecker::new(file_locks, resource_locks);
let result = checker
.can_write_file(Path::new("/test/project/src/main.rs"), "agent-1")
.await;
assert!(matches!(result, ConflictCheck::Clear));
}
#[tokio::test]
async fn test_build_blocked_by_file_write() {
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
file_locks
.acquire_lock("agent-2", "/test/project/src/main.rs", LockType::Write)
.await
.unwrap();
let checker = ResourceChecker::new(file_locks, resource_locks);
let scope = ResourceScope::Project(PathBuf::from("/test/project"));
let result = checker.can_start_build(&scope, "agent-1").await;
assert!(result.is_blocked());
let conflicts = result.conflicts();
assert_eq!(conflicts.len(), 1);
assert!(matches!(
conflicts[0].conflict_type,
ResourceConflictType::FileWriteBlocksBuild { .. }
));
}
#[tokio::test]
async fn test_file_write_blocked_by_build() {
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
resource_locks
.acquire_resource(
"agent-2",
ResourceType::Build,
ResourceScope::Project(PathBuf::from("/test/project")),
"cargo build",
)
.await
.unwrap();
let checker = ResourceChecker::new(file_locks, resource_locks);
let result = checker
.can_write_file(Path::new("/test/project/src/main.rs"), "agent-1")
.await;
assert!(result.is_blocked());
let conflicts = result.conflicts();
assert_eq!(conflicts.len(), 1);
assert!(matches!(
conflicts[0].conflict_type,
ResourceConflictType::BuildBlocksFileWrite
));
}
#[tokio::test]
async fn test_same_agent_no_conflict() {
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
file_locks
.acquire_lock("agent-1", "/test/project/src/main.rs", LockType::Write)
.await
.unwrap();
let checker = ResourceChecker::new(file_locks, resource_locks);
let scope = ResourceScope::Project(PathBuf::from("/test/project"));
let result = checker.can_start_build(&scope, "agent-1").await;
assert!(matches!(result, ConflictCheck::Clear));
}
}