1use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::time::Instant;
13
14use crate::communication::{ConflictInfo, ConflictType};
15use crate::file_locks::{FileLockManager, LockType};
16use crate::operation_tracker::OperationTracker;
17use crate::resource_locks::{ResourceLockManager, ResourceScope, ResourceType};
18
19#[derive(Debug, Clone)]
21pub enum ConflictCheck {
22 Clear,
24 Blocked(Vec<Conflict>),
26 Warning(Vec<Conflict>),
28}
29
30impl ConflictCheck {
31 pub fn can_proceed(&self) -> bool {
33 matches!(self, ConflictCheck::Clear | ConflictCheck::Warning(_))
34 }
35
36 pub fn is_blocked(&self) -> bool {
38 matches!(self, ConflictCheck::Blocked(_))
39 }
40
41 pub fn conflicts(&self) -> &[Conflict] {
43 match self {
44 ConflictCheck::Clear => &[],
45 ConflictCheck::Blocked(c) | ConflictCheck::Warning(c) => c,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct Conflict {
53 pub conflict_type: ResourceConflictType,
55 pub holder_agent: String,
57 pub resource: String,
59 pub started_at: Instant,
61 pub status: String,
63 pub description: String,
65}
66
67impl Conflict {
68 pub fn to_conflict_info(&self) -> ConflictInfo {
70 ConflictInfo {
71 conflict_type: match &self.conflict_type {
72 ResourceConflictType::FileWriteBlocksBuild { path } => {
73 ConflictType::FileWriteBlocksBuild { path: path.clone() }
74 }
75 ResourceConflictType::BuildBlocksFileWrite => ConflictType::BuildBlocksFileWrite,
76 ResourceConflictType::TestBlocksFileWrite => ConflictType::TestBlocksFileWrite,
77 ResourceConflictType::GitBlocksFileWrite => ConflictType::GitBlocksFileWrite,
78 ResourceConflictType::FileWriteBlocksGit { path } => {
79 ConflictType::FileWriteBlocksGit { path: path.clone() }
80 }
81 ResourceConflictType::BuildBlocksGit => ConflictType::BuildBlocksGit,
82 },
83 holder_agent: self.holder_agent.clone(),
84 resource: self.resource.clone(),
85 duration_secs: self.started_at.elapsed().as_secs(),
86 status: self.status.clone(),
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub enum ResourceConflictType {
94 FileWriteBlocksBuild {
96 path: PathBuf,
98 },
99 BuildBlocksFileWrite,
101 TestBlocksFileWrite,
103 GitBlocksFileWrite,
105 FileWriteBlocksGit {
107 path: PathBuf,
109 },
110 BuildBlocksGit,
112}
113
114#[derive(Debug, Clone)]
116pub enum ProposedOperation {
117 FileWrite {
119 path: PathBuf,
121 agent_id: String,
123 },
124 Build {
126 scope: ResourceScope,
128 agent_id: String,
130 },
131 Test {
133 scope: ResourceScope,
135 agent_id: String,
137 },
138 GitStaging {
140 scope: ResourceScope,
142 agent_id: String,
144 },
145 GitCommit {
147 scope: ResourceScope,
149 agent_id: String,
151 },
152 GitPush {
154 scope: ResourceScope,
156 agent_id: String,
158 },
159 GitPull {
161 scope: ResourceScope,
163 agent_id: String,
165 },
166}
167
168impl ProposedOperation {
169 pub fn agent_id(&self) -> &str {
171 match self {
172 ProposedOperation::FileWrite { agent_id, .. }
173 | ProposedOperation::Build { agent_id, .. }
174 | ProposedOperation::Test { agent_id, .. }
175 | ProposedOperation::GitStaging { agent_id, .. }
176 | ProposedOperation::GitCommit { agent_id, .. }
177 | ProposedOperation::GitPush { agent_id, .. }
178 | ProposedOperation::GitPull { agent_id, .. } => agent_id,
179 }
180 }
181}
182
183pub struct ResourceChecker {
190 file_locks: Arc<FileLockManager>,
191 resource_locks: Arc<ResourceLockManager>,
192 _operation_tracker: Option<Arc<OperationTracker>>,
193 source_patterns: Vec<String>,
195}
196
197impl ResourceChecker {
198 pub fn new(file_locks: Arc<FileLockManager>, resource_locks: Arc<ResourceLockManager>) -> Self {
200 Self {
201 file_locks,
202 resource_locks,
203 _operation_tracker: None,
204 source_patterns: default_source_patterns(),
205 }
206 }
207
208 pub fn with_operation_tracker(
210 file_locks: Arc<FileLockManager>,
211 resource_locks: Arc<ResourceLockManager>,
212 operation_tracker: Arc<OperationTracker>,
213 ) -> Self {
214 Self {
215 file_locks,
216 resource_locks,
217 _operation_tracker: Some(operation_tracker),
218 source_patterns: default_source_patterns(),
219 }
220 }
221
222 pub fn with_source_patterns(mut self, patterns: Vec<String>) -> Self {
224 self.source_patterns = patterns;
225 self
226 }
227
228 pub async fn can_start_build(&self, scope: &ResourceScope, agent_id: &str) -> ConflictCheck {
233 let file_locks = self.file_locks.list_locks().await;
234
235 let mut conflicts = Vec::new();
236
237 for (path, lock_info) in file_locks {
238 if lock_info.agent_id == agent_id {
240 continue;
241 }
242
243 if lock_info.lock_type != LockType::Write {
245 continue;
246 }
247
248 if self.is_in_scope(&path, scope) && self.is_source_file(&path) {
250 conflicts.push(Conflict {
251 conflict_type: ResourceConflictType::FileWriteBlocksBuild {
252 path: path.clone(),
253 },
254 holder_agent: lock_info.agent_id.clone(),
255 resource: path.display().to_string(),
256 started_at: lock_info.acquired_at,
257 status: "File locked for editing".to_string(),
258 description: format!("Write lock on {}", path.display()),
259 });
260 }
261 }
262
263 if conflicts.is_empty() {
264 ConflictCheck::Clear
265 } else {
266 ConflictCheck::Blocked(conflicts)
267 }
268 }
269
270 pub async fn can_write_file(&self, path: &Path, agent_id: &str) -> ConflictCheck {
275 let resource_locks = self.resource_locks.list_locks().await;
276
277 let mut conflicts = Vec::new();
278
279 let file_scope = self.scope_for_path(path);
281
282 for lock_info in resource_locks {
283 if lock_info.agent_id == agent_id {
285 continue;
286 }
287
288 if !self.scopes_overlap(&lock_info.scope, &file_scope) {
290 continue;
291 }
292
293 if !self.is_source_file(path) {
295 continue;
296 }
297
298 match lock_info.resource_type {
300 ResourceType::Build | ResourceType::BuildTest => {
301 conflicts.push(Conflict {
302 conflict_type: ResourceConflictType::BuildBlocksFileWrite,
303 holder_agent: lock_info.agent_id.clone(),
304 resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
305 started_at: lock_info.acquired_at,
306 status: lock_info.status.clone(),
307 description: lock_info.description.clone(),
308 });
309 }
310 ResourceType::Test => {
311 conflicts.push(Conflict {
312 conflict_type: ResourceConflictType::TestBlocksFileWrite,
313 holder_agent: lock_info.agent_id.clone(),
314 resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
315 started_at: lock_info.acquired_at,
316 status: lock_info.status.clone(),
317 description: lock_info.description.clone(),
318 });
319 }
320 ResourceType::GitIndex
321 | ResourceType::GitCommit
322 | ResourceType::GitRemoteWrite
323 | ResourceType::GitRemoteMerge
324 | ResourceType::GitBranch
325 | ResourceType::GitDestructive => {
326 if lock_info.resource_type == ResourceType::GitRemoteMerge
328 || lock_info.resource_type == ResourceType::GitDestructive
329 {
330 conflicts.push(Conflict {
331 conflict_type: ResourceConflictType::GitBlocksFileWrite,
332 holder_agent: lock_info.agent_id.clone(),
333 resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
334 started_at: lock_info.acquired_at,
335 status: lock_info.status.clone(),
336 description: lock_info.description.clone(),
337 });
338 }
339 }
340 }
341 }
342
343 if conflicts.is_empty() {
344 ConflictCheck::Clear
345 } else {
346 ConflictCheck::Blocked(conflicts)
347 }
348 }
349
350 pub async fn can_start_git_operation(
356 &self,
357 git_op: ResourceType,
358 scope: &ResourceScope,
359 agent_id: &str,
360 ) -> ConflictCheck {
361 let mut conflicts = Vec::new();
362
363 if matches!(
365 git_op,
366 ResourceType::GitIndex | ResourceType::GitCommit | ResourceType::GitRemoteWrite
367 ) {
368 let file_locks = self.file_locks.list_locks().await;
369
370 for (path, lock_info) in file_locks {
371 if lock_info.agent_id == agent_id {
372 continue;
373 }
374 if lock_info.lock_type != LockType::Write {
375 continue;
376 }
377 if self.is_in_scope(&path, scope) && self.is_source_file(&path) {
378 conflicts.push(Conflict {
379 conflict_type: ResourceConflictType::FileWriteBlocksGit {
380 path: path.clone(),
381 },
382 holder_agent: lock_info.agent_id.clone(),
383 resource: path.display().to_string(),
384 started_at: lock_info.acquired_at,
385 status: "File locked for editing".to_string(),
386 description: format!("Write lock on {}", path.display()),
387 });
388 }
389 }
390 }
391
392 if matches!(
394 git_op,
395 ResourceType::GitCommit | ResourceType::GitRemoteWrite
396 ) {
397 let resource_locks = self.resource_locks.list_locks().await;
398
399 for lock_info in resource_locks {
400 if lock_info.agent_id == agent_id {
401 continue;
402 }
403 if !self.scopes_overlap(&lock_info.scope, scope) {
404 continue;
405 }
406
407 if matches!(
408 lock_info.resource_type,
409 ResourceType::Build | ResourceType::Test | ResourceType::BuildTest
410 ) {
411 conflicts.push(Conflict {
412 conflict_type: ResourceConflictType::BuildBlocksGit,
413 holder_agent: lock_info.agent_id.clone(),
414 resource: format!("{} ({})", lock_info.resource_type, lock_info.scope),
415 started_at: lock_info.acquired_at,
416 status: lock_info.status.clone(),
417 description: lock_info.description.clone(),
418 });
419 }
420 }
421 }
422
423 if conflicts.is_empty() {
424 ConflictCheck::Clear
425 } else {
426 ConflictCheck::Blocked(conflicts)
427 }
428 }
429
430 pub async fn check_conflicts(&self, operation: &ProposedOperation) -> ConflictCheck {
432 match operation {
433 ProposedOperation::FileWrite { path, agent_id } => {
434 self.can_write_file(path, agent_id).await
435 }
436 ProposedOperation::Build { scope, agent_id } => {
437 self.can_start_build(scope, agent_id).await
438 }
439 ProposedOperation::Test { scope, agent_id } => {
440 self.can_start_build(scope, agent_id).await
442 }
443 ProposedOperation::GitStaging { scope, agent_id } => {
444 self.can_start_git_operation(ResourceType::GitIndex, scope, agent_id)
445 .await
446 }
447 ProposedOperation::GitCommit { scope, agent_id } => {
448 self.can_start_git_operation(ResourceType::GitCommit, scope, agent_id)
449 .await
450 }
451 ProposedOperation::GitPush { scope, agent_id } => {
452 self.can_start_git_operation(ResourceType::GitRemoteWrite, scope, agent_id)
453 .await
454 }
455 ProposedOperation::GitPull { scope, agent_id } => {
456 self.can_start_git_operation(ResourceType::GitRemoteMerge, scope, agent_id)
457 .await
458 }
459 }
460 }
461
462 pub async fn get_build_blockers(&self, scope: &ResourceScope, agent_id: &str) -> Vec<Conflict> {
464 match self.can_start_build(scope, agent_id).await {
465 ConflictCheck::Blocked(conflicts) => conflicts,
466 _ => Vec::new(),
467 }
468 }
469
470 pub async fn get_file_write_blockers(&self, path: &Path, agent_id: &str) -> Vec<Conflict> {
472 match self.can_write_file(path, agent_id).await {
473 ConflictCheck::Blocked(conflicts) => conflicts,
474 _ => Vec::new(),
475 }
476 }
477
478 fn is_in_scope(&self, path: &Path, scope: &ResourceScope) -> bool {
482 match scope {
483 ResourceScope::Global => true,
484 ResourceScope::Project(project_path) => path.starts_with(project_path),
485 }
486 }
487
488 fn scope_for_path(&self, path: &Path) -> ResourceScope {
490 let mut current = path.parent();
492 while let Some(dir) = current {
493 if dir.join("Cargo.toml").exists()
494 || dir.join("package.json").exists()
495 || dir.join(".git").exists()
496 {
497 return ResourceScope::Project(dir.to_path_buf());
498 }
499 current = dir.parent();
500 }
501 ResourceScope::Global
502 }
503
504 fn scopes_overlap(&self, scope1: &ResourceScope, scope2: &ResourceScope) -> bool {
506 match (scope1, scope2) {
507 (ResourceScope::Global, _) | (_, ResourceScope::Global) => true,
508 (ResourceScope::Project(p1), ResourceScope::Project(p2)) => {
509 p1.starts_with(p2) || p2.starts_with(p1)
510 }
511 }
512 }
513
514 fn is_source_file(&self, path: &Path) -> bool {
516 let path_str = path.to_string_lossy();
517
518 for pattern in &self.source_patterns {
520 if matches_pattern(&path_str, pattern) {
521 return true;
522 }
523 }
524
525 if let Some(ext) = path.extension() {
527 let ext = ext.to_string_lossy().to_lowercase();
528 matches!(
529 ext.as_str(),
530 "rs" | "ts"
531 | "tsx"
532 | "js"
533 | "jsx"
534 | "py"
535 | "go"
536 | "java"
537 | "c"
538 | "cpp"
539 | "h"
540 | "hpp"
541 | "cs"
542 | "swift"
543 | "kt"
544 | "scala"
545 | "rb"
546 | "php"
547 )
548 } else {
549 false
550 }
551 }
552}
553
554fn default_source_patterns() -> Vec<String> {
556 vec![
557 "src/**/*".to_string(),
558 "lib/**/*".to_string(),
559 "crates/**/*".to_string(),
560 "packages/**/*".to_string(),
561 "app/**/*".to_string(),
562 ]
563}
564
565fn matches_pattern(path: &str, pattern: &str) -> bool {
567 if pattern.contains("**") {
568 let parts: Vec<&str> = pattern.split("**").collect();
570 if parts.len() == 2 {
571 let prefix = parts[0].trim_end_matches('/');
572 let suffix = parts[1].trim_start_matches('/');
573
574 let has_prefix = prefix.is_empty() || path.starts_with(prefix);
575 let has_suffix = suffix.is_empty()
576 || suffix == "*"
577 || path.ends_with(suffix.trim_start_matches('*'));
578
579 return has_prefix && has_suffix;
580 }
581 }
582
583 if pattern.contains('*') {
584 let parts: Vec<&str> = pattern.split('*').collect();
586 if parts.len() == 2 {
587 return path.starts_with(parts[0]) && path.ends_with(parts[1]);
588 }
589 }
590
591 path == pattern
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn test_matches_pattern() {
601 assert!(matches_pattern("src/main.rs", "src/**/*"));
602 assert!(matches_pattern("src/lib/utils.rs", "src/**/*"));
603 assert!(matches_pattern("crates/foo/src/lib.rs", "crates/**/*"));
604 assert!(!matches_pattern("target/debug/main", "src/**/*"));
605 }
606
607 #[test]
608 fn test_is_source_file() {
609 let checker = ResourceChecker::new(
610 Arc::new(FileLockManager::new()),
611 Arc::new(ResourceLockManager::new()),
612 );
613
614 assert!(checker.is_source_file(Path::new("src/main.rs")));
615 assert!(checker.is_source_file(Path::new("lib/index.ts")));
616 assert!(checker.is_source_file(Path::new("app.py")));
617 assert!(!checker.is_source_file(Path::new("README.md")));
618 assert!(!checker.is_source_file(Path::new("Cargo.toml")));
619 }
620
621 #[test]
622 fn test_scopes_overlap() {
623 let checker = ResourceChecker::new(
624 Arc::new(FileLockManager::new()),
625 Arc::new(ResourceLockManager::new()),
626 );
627
628 assert!(checker.scopes_overlap(&ResourceScope::Global, &ResourceScope::Global));
630 assert!(checker.scopes_overlap(
631 &ResourceScope::Global,
632 &ResourceScope::Project(PathBuf::from("/foo"))
633 ));
634
635 assert!(checker.scopes_overlap(
637 &ResourceScope::Project(PathBuf::from("/foo")),
638 &ResourceScope::Project(PathBuf::from("/foo"))
639 ));
640 assert!(checker.scopes_overlap(
641 &ResourceScope::Project(PathBuf::from("/foo")),
642 &ResourceScope::Project(PathBuf::from("/foo/bar"))
643 ));
644 assert!(!checker.scopes_overlap(
645 &ResourceScope::Project(PathBuf::from("/foo")),
646 &ResourceScope::Project(PathBuf::from("/baz"))
647 ));
648 }
649
650 #[tokio::test]
651 async fn test_can_start_build_no_conflicts() {
652 let file_locks = Arc::new(FileLockManager::new());
653 let resource_locks = Arc::new(ResourceLockManager::new());
654 let checker = ResourceChecker::new(file_locks, resource_locks);
655
656 let scope = ResourceScope::Project(PathBuf::from("/test/project"));
657 let result = checker.can_start_build(&scope, "agent-1").await;
658
659 assert!(matches!(result, ConflictCheck::Clear));
660 }
661
662 #[tokio::test]
663 async fn test_can_write_file_no_conflicts() {
664 let file_locks = Arc::new(FileLockManager::new());
665 let resource_locks = Arc::new(ResourceLockManager::new());
666 let checker = ResourceChecker::new(file_locks, resource_locks);
667
668 let result = checker
669 .can_write_file(Path::new("/test/project/src/main.rs"), "agent-1")
670 .await;
671
672 assert!(matches!(result, ConflictCheck::Clear));
673 }
674
675 #[tokio::test]
676 async fn test_build_blocked_by_file_write() {
677 let file_locks = Arc::new(FileLockManager::new());
678 let resource_locks = Arc::new(ResourceLockManager::new());
679
680 file_locks
682 .acquire_lock("agent-2", "/test/project/src/main.rs", LockType::Write)
683 .await
684 .unwrap();
685
686 let checker = ResourceChecker::new(file_locks, resource_locks);
687
688 let scope = ResourceScope::Project(PathBuf::from("/test/project"));
689 let result = checker.can_start_build(&scope, "agent-1").await;
690
691 assert!(result.is_blocked());
692 let conflicts = result.conflicts();
693 assert_eq!(conflicts.len(), 1);
694 assert!(matches!(
695 conflicts[0].conflict_type,
696 ResourceConflictType::FileWriteBlocksBuild { .. }
697 ));
698 }
699
700 #[tokio::test]
701 async fn test_file_write_blocked_by_build() {
702 let file_locks = Arc::new(FileLockManager::new());
703 let resource_locks = Arc::new(ResourceLockManager::new());
704
705 resource_locks
707 .acquire_resource(
708 "agent-2",
709 ResourceType::Build,
710 ResourceScope::Project(PathBuf::from("/test/project")),
711 "cargo build",
712 )
713 .await
714 .unwrap();
715
716 let checker = ResourceChecker::new(file_locks, resource_locks);
717
718 let result = checker
719 .can_write_file(Path::new("/test/project/src/main.rs"), "agent-1")
720 .await;
721
722 assert!(result.is_blocked());
723 let conflicts = result.conflicts();
724 assert_eq!(conflicts.len(), 1);
725 assert!(matches!(
726 conflicts[0].conflict_type,
727 ResourceConflictType::BuildBlocksFileWrite
728 ));
729 }
730
731 #[tokio::test]
732 async fn test_same_agent_no_conflict() {
733 let file_locks = Arc::new(FileLockManager::new());
734 let resource_locks = Arc::new(ResourceLockManager::new());
735
736 file_locks
738 .acquire_lock("agent-1", "/test/project/src/main.rs", LockType::Write)
739 .await
740 .unwrap();
741
742 let checker = ResourceChecker::new(file_locks, resource_locks);
743
744 let scope = ResourceScope::Project(PathBuf::from("/test/project"));
745 let result = checker.can_start_build(&scope, "agent-1").await;
746
747 assert!(matches!(result, ConflictCheck::Clear));
749 }
750}