agpm_cli/resolver/mod.rs
1//! Dependency resolution and conflict detection for AGPM.
2//!
3//! This module implements the core dependency resolution algorithm that transforms
4//! manifest dependencies into locked versions. It handles version constraint solving,
5//! conflict detection, transitive dependency resolution,
6//! parallel source synchronization, and relative path preservation during installation.
7//!
8//! # Service-Based Architecture
9//!
10//! This resolver has been refactored to use a service-based architecture:
11//! - **ResolutionCore**: Shared immutable state
12//! - **VersionResolutionService**: Git operations and version resolution
13//! - **PatternExpansionService**: Glob pattern expansion
14//! - **TransitiveDependencyService**: Transitive dependency resolution
15//! - **ConflictService**: Conflict detection
16//! - **ResourceFetchingService**: Resource content fetching
17
18// Declare service modules
19pub mod backtracking;
20pub mod conflict_service;
21pub mod dependency_graph;
22mod dependency_processing;
23mod entry_builder;
24mod incremental_update;
25pub mod lockfile_builder;
26pub mod path_resolver;
27pub mod pattern_expander;
28pub mod resource_service;
29pub mod sha_conflict_detector;
30pub mod skills;
31pub mod source_context;
32pub mod transitive_extractor;
33pub mod transitive_resolver;
34pub mod types;
35pub mod version_resolver;
36
37#[cfg(test)]
38mod tests;
39
40// Re-export utility functions for compatibility
41pub use path_resolver::{extract_meaningful_path, is_file_relative_path, normalize_bare_filename};
42
43use std::collections::HashMap;
44use std::path::Path;
45use std::sync::Arc;
46
47use anyhow::{Context, Result};
48use dashmap::DashMap;
49
50use crate::cache::Cache;
51use crate::core::{OperationContext, ResourceType};
52use crate::lockfile::{LockFile, LockedResource};
53use crate::manifest::{Manifest, ResourceDependency};
54use crate::source::SourceManager;
55
56// Re-export services for external use
57pub use conflict_service::ConflictService;
58pub use pattern_expander::PatternExpansionService;
59pub use resource_service::ResourceFetchingService;
60pub use types::ResolutionCore;
61pub use version_resolver::{
62 VersionResolutionService, VersionResolver as VersionResolverExport, find_best_matching_tag,
63 is_version_constraint, parse_tags_to_versions,
64};
65
66// Legacy re-exports for compatibility
67pub use dependency_graph::{DependencyGraph, DependencyNode};
68pub use lockfile_builder::LockfileBuilder;
69pub use pattern_expander::{expand_pattern_to_concrete_deps, generate_dependency_name};
70pub use types::{
71 ConflictDetectionKey, DependencyKey, ManifestOverride, ManifestOverrideIndex, OverrideKey,
72 ResolutionContext, ResolvedDependenciesMap, ResolvedDependencyInfo, TransitiveContext,
73};
74
75pub use version_resolver::{PreparedSourceVersion, VersionResolver, WorktreeManager};
76
77/// Main dependency resolver with service-based architecture.
78///
79/// This orchestrates multiple specialized services to handle different aspects
80/// of the dependency resolution process while maintaining compatibility
81/// with existing interfaces.
82///
83/// # Architecture
84///
85/// The resolver follows a modular service pattern where each complex aspect
86/// of resolution is delegated to a specialized service:
87/// - [`VersionResolutionService`] handles Git operations and batch SHA resolution
88/// - [`PatternExpansionService`] expands glob patterns into concrete dependencies
89/// - Version conflict detection identifies and reports version conflicts
90///
91/// # Resolution Process
92///
93/// The resolution occurs in distinct phases:
94///
95/// 1. **Collection Phase**: Extract dependencies from the project manifest
96/// 2. **Version Resolution Phase**: Batch resolve all version constraints to commit SHAs
97/// 3. **Pattern Expansion Phase**: Expand glob patterns (e.g., `agents/*.md`) into individual resources
98/// 4. **Transitive Resolution Phase** (optional): Resolve dependencies declared within resources
99/// 5. **Conflict Detection Phase**: Detect and report version conflicts across the dependency graph
100///
101/// # Examples
102///
103/// ```ignore
104/// use agpm_cli::resolver::DependencyResolver;
105/// use agpm_cli::manifest::Manifest;
106/// use agpm_cli::cache::Cache;
107///
108/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
109/// // Load manifest and create cache
110/// let manifest = Manifest::load_from_file("agpm.toml")?;
111/// let cache = Cache::new()?;
112///
113/// // Create resolver and resolve dependencies
114/// let mut resolver = DependencyResolver::new(manifest, cache).await?;
115/// let lockfile = resolver.resolve().await?;
116///
117/// println!("Resolved {} dependencies", lockfile.total_resources());
118/// # Ok(())
119/// # }
120/// ```
121///
122/// # Key Features
123///
124/// - **Parallel Processing**: Configurable concurrency for performance
125/// - **SHA-based Deduplication**: Shared worktrees for identical commits
126/// - **Transitive Dependencies**: Optional resolution of dependencies of dependencies
127/// - **Version Constraints**: Support for semver-style constraints (`^1.0`, `~2.1`)
128/// - **Pattern Support**: Glob patterns for bulk dependency inclusion
129/// - **Conflict Detection**: Comprehensive detection of version conflicts
130///
131/// # Related Services
132///
133/// - [`VersionResolutionService`]: Git operations and SHA resolution
134/// - [`PatternExpansionService`]: Glob pattern handling
135/// - Service container for transitive resolution (transitive_resolver::ResolutionServices)
136/// - Version conflict detection and reporting (conflict_detector::ConflictDetector)
137pub struct DependencyResolver {
138 /// Core shared context with immutable state
139 core: ResolutionCore,
140
141 /// Version resolution and Git operations service
142 version_service: VersionResolutionService,
143
144 /// Pattern expansion service for glob dependencies
145 pattern_service: PatternExpansionService,
146
147 /// Conflict detector for version conflicts
148 conflict_detector: crate::version::conflict::ConflictDetector,
149
150 /// SHA-based conflict detector
151 sha_conflict_detector: crate::resolver::sha_conflict_detector::ShaConflictDetector,
152
153 /// Dependency tracking state (concurrent)
154 dependency_map: Arc<DashMap<DependencyKey, Vec<String>>>,
155
156 /// Pattern alias tracking for expanded patterns (concurrent)
157 pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
158
159 /// Transitive dependency custom names (concurrent)
160 transitive_custom_names: Arc<DashMap<DependencyKey, String>>,
161
162 /// Track if sources have been pre-synced to avoid duplicate work
163 /// Uses AtomicBool with Acquire/Release ordering for thread-safe synchronization during parallel dependency resolution
164 sources_pre_synced: std::sync::atomic::AtomicBool,
165
166 /// Tracks resolved SHAs for conflict detection
167 /// Key: (resource_id, required_by, name)
168 /// Value: (version_constraint, resolved_sha, parent_version, parent_sha, resolution_mode)
169 /// Uses DashMap for concurrent access during parallel dependency resolution
170 resolved_deps_for_conflict_check: ResolvedDependenciesMap,
171
172 /// Reverse lookup from dependency reference → parents that require it.
173 ///
174 /// Key: Dependency reference (e.g., "agents/helper", "snippet:snippets/foo")
175 /// Value: List of parent resource IDs that depend on this resource
176 ///
177 /// Populated during resolution to enable efficient parent metadata lookups
178 /// without searching through all resolved dependencies.
179 /// Uses DashMap for concurrent access during parallel dependency resolution
180 reverse_dependency_map: std::sync::Arc<dashmap::DashMap<String, Vec<String>>>,
181}
182
183impl DependencyResolver {
184 /// Initialize a DependencyResolver with the given core and services.
185 ///
186 /// This private helper function centralizes the struct initialization logic
187 /// to reduce duplication across constructors.
188 ///
189 /// # Arguments
190 ///
191 /// * `core` - Resolution core with manifest, cache, and source manager
192 /// * `version_service` - Version resolution service
193 /// * `pattern_service` - Pattern expansion service
194 fn init_dependencies(
195 core: ResolutionCore,
196 version_service: VersionResolutionService,
197 pattern_service: PatternExpansionService,
198 ) -> Result<Self> {
199 Ok(Self {
200 core,
201 version_service,
202 pattern_service,
203 conflict_detector: crate::version::conflict::ConflictDetector::new(),
204 sha_conflict_detector: crate::resolver::sha_conflict_detector::ShaConflictDetector::new(
205 ),
206 dependency_map: Arc::new(DashMap::new()),
207 pattern_alias_map: Arc::new(DashMap::new()),
208 transitive_custom_names: Arc::new(DashMap::new()),
209 sources_pre_synced: std::sync::atomic::AtomicBool::new(false),
210 resolved_deps_for_conflict_check: Arc::new(DashMap::new()),
211 reverse_dependency_map: std::sync::Arc::new(dashmap::DashMap::new()),
212 })
213 }
214
215 /// Create a new dependency resolver.
216 ///
217 /// # Arguments
218 ///
219 /// * `manifest` - Project manifest with dependencies
220 /// * `cache` - Cache for Git operations and worktrees
221 ///
222 /// # Errors
223 ///
224 /// Returns an error if source manager cannot be created
225 pub async fn new(manifest: Manifest, cache: Cache) -> Result<Self> {
226 Self::new_with_context(manifest, cache, None).await
227 }
228
229 /// Create a new dependency resolver with operation context.
230 ///
231 /// # Arguments
232 ///
233 /// * `manifest` - Project manifest with dependencies
234 /// * `cache` - Cache for Git operations and worktrees
235 /// * `operation_context` - Optional context for warning deduplication
236 ///
237 /// # Errors
238 ///
239 /// Returns an error if source manager cannot be created
240 pub async fn new_with_context(
241 manifest: Manifest,
242 cache: Cache,
243 operation_context: Option<Arc<OperationContext>>,
244 ) -> Result<Self> {
245 // Create source manager from manifest
246 let source_manager = SourceManager::from_manifest(&manifest)?;
247
248 // Create resolution core with shared state
249 let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
250
251 // Initialize all services
252 let version_service = VersionResolutionService::new(core.cache().clone());
253 let pattern_service = PatternExpansionService::new();
254
255 Self::init_dependencies(core, version_service, pattern_service)
256 }
257
258 /// Create a new resolver with global configuration support.
259 ///
260 /// This loads both manifest sources and global sources from `~/.agpm/config.toml`.
261 ///
262 /// # Arguments
263 ///
264 /// * `manifest` - Project manifest with dependencies
265 /// * `cache` - Cache for Git operations and worktrees
266 ///
267 /// # Errors
268 ///
269 /// Returns an error if global configuration cannot be loaded
270 pub async fn new_with_global(manifest: Manifest, cache: Cache) -> Result<Self> {
271 Self::new_with_global_concurrency(manifest, cache, None, None).await
272 }
273
274 /// Creates a new dependency resolver with global config and custom concurrency limit.
275 ///
276 /// # Arguments
277 ///
278 /// * `manifest` - Project manifest with dependencies
279 /// * `cache` - Cache for Git operations and worktrees
280 /// * `max_concurrency` - Optional concurrency limit for parallel operations
281 /// * `operation_context` - Optional context for warning deduplication
282 ///
283 /// # Errors
284 ///
285 /// Returns an error if global configuration cannot be loaded
286 pub async fn new_with_global_concurrency(
287 manifest: Manifest,
288 cache: Cache,
289 max_concurrency: Option<usize>,
290 operation_context: Option<Arc<OperationContext>>,
291 ) -> Result<Self> {
292 let source_manager = SourceManager::from_manifest_with_global(&manifest).await?;
293
294 let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
295
296 let version_service = if let Some(concurrency) = max_concurrency {
297 VersionResolutionService::with_concurrency(core.cache().clone(), concurrency)
298 } else {
299 VersionResolutionService::new(core.cache().clone())
300 };
301 let pattern_service = PatternExpansionService::new();
302
303 Self::init_dependencies(core, version_service, pattern_service)
304 }
305
306 /// Creates a new dependency resolver with custom cache directory.
307 ///
308 /// # Arguments
309 ///
310 /// * `cache` - Cache for Git operations and worktrees
311 ///
312 /// # Errors
313 ///
314 /// Returns an error if source manager cannot be created
315 pub async fn with_cache(manifest: Manifest, cache: Cache) -> Result<Self> {
316 Self::new_with_context(manifest, cache, None).await
317 }
318
319 /// Create a new resolver with global configuration and operation context.
320 ///
321 /// # Arguments
322 ///
323 /// * `manifest` - Project manifest with dependencies
324 /// * `cache` - Cache for Git operations and worktrees
325 /// * `operation_context` - Optional context for warning deduplication
326 ///
327 /// # Errors
328 ///
329 /// Returns an error if global configuration cannot be loaded
330 pub async fn new_with_global_context(
331 manifest: Manifest,
332 cache: Cache,
333 operation_context: Option<Arc<OperationContext>>,
334 ) -> Result<Self> {
335 Self::new_with_global_concurrency(manifest, cache, None, operation_context).await
336 }
337
338 /// Get a reference to the resolution core.
339 pub fn core(&self) -> &ResolutionCore {
340 &self.core
341 }
342
343 /// Resolve all dependencies and generate a complete lockfile.
344 ///
345 /// Performs dependency resolution with automatic conflict detection and
346 /// backtracking to find compatible versions when conflicts occur.
347 ///
348 /// # Errors
349 ///
350 /// Returns an error if any step of resolution fails
351 pub async fn resolve(&mut self) -> Result<LockFile> {
352 self.resolve_with_options(true, None).await
353 }
354
355 /// Resolve dependencies with transitive resolution option.
356 ///
357 /// # Arguments
358 ///
359 /// * `enable_transitive` - Whether to resolve transitive dependencies
360 ///
361 /// # Errors
362 ///
363 /// Returns an error if resolution fails
364 pub async fn resolve_with_options(
365 &mut self,
366 enable_transitive: bool,
367 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
368 ) -> Result<LockFile> {
369 // Phase 1: Preparation and manifest loading
370 let (base_deps, mut lockfile) = self.prepare_resolution(&progress).await?;
371
372 // Phase 2: Pre-sync sources
373 self.pre_sync_sources_if_needed(&base_deps, progress.clone()).await?;
374
375 // Phase 3: Resolve transitive dependencies
376 let all_deps = self
377 .resolve_transitive_dependencies_phase(&base_deps, enable_transitive, progress.clone())
378 .await?;
379
380 // Phase 4: Resolve individual dependencies
381 self.resolve_individual_dependencies(&all_deps, &mut lockfile, progress.clone()).await?;
382
383 // Phase 5: Handle conflicts and backtracking
384 self.handle_conflicts_and_backtracking(&mut lockfile).await?;
385
386 // Phase 6: Final post-processing
387 self.finalize_resolution(&mut lockfile, &progress)?;
388
389 Ok(lockfile)
390 }
391
392 /// Phase 1: Prepare resolution context and extract base dependencies
393 ///
394 /// Returns the base dependencies from the manifest and an initialized lockfile.
395 async fn prepare_resolution(
396 &mut self,
397 progress: &Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
398 ) -> Result<(Vec<(String, ResourceDependency, ResourceType)>, LockFile)> {
399 // Clear state from previous resolution
400 self.resolved_deps_for_conflict_check.clear();
401 self.reverse_dependency_map.clear();
402 self.conflict_detector = crate::version::conflict::ConflictDetector::new();
403
404 let mut lockfile = LockFile::new();
405
406 // Add sources to lockfile
407 for (name, url) in &self.core.manifest().sources {
408 lockfile.add_source(name.clone(), url.clone(), String::new());
409 }
410
411 // Extract dependencies from manifest with types
412 let base_deps: Vec<(String, ResourceDependency, ResourceType)> = self
413 .core
414 .manifest()
415 .all_dependencies_with_types()
416 .into_iter()
417 .map(|(name, dep, resource_type)| (name.to_string(), dep.into_owned(), resource_type))
418 .collect();
419
420 // Start the ResolvingDependencies phase with windowed tracking
421 // This phase includes: transitive resolution (Phase 3), individual resolution (Phase 4),
422 // and conflict detection (Phase 6). We start with base deps count as initial estimate.
423 let window_size = 7;
424 if let Some(pm) = progress {
425 tracing::debug!(
426 "Starting ResolvingDependencies phase with windowed tracking: {} base deps, {} slots",
427 base_deps.len(),
428 window_size
429 );
430 pm.start_phase_with_active_tracking(
431 crate::utils::InstallationPhase::ResolvingDependencies,
432 base_deps.len(),
433 window_size,
434 );
435 }
436
437 Ok((base_deps, lockfile))
438 }
439
440 /// Phase 2: Pre-sync all sources if not already done
441 async fn pre_sync_sources_if_needed(
442 &mut self,
443 base_deps: &[(String, ResourceDependency, ResourceType)],
444 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
445 ) -> Result<()> {
446 if !self.sources_pre_synced.load(std::sync::atomic::Ordering::Acquire) {
447 let deps_for_sync: Vec<(String, ResourceDependency)> =
448 base_deps.iter().map(|(name, dep, _)| (name.clone(), dep.clone())).collect();
449 self.version_service.pre_sync_sources(&self.core, &deps_for_sync, progress).await?;
450 self.sources_pre_synced.store(true, std::sync::atomic::Ordering::Release);
451 }
452 Ok(())
453 }
454
455 /// Phase 3: Resolve transitive dependencies
456 async fn resolve_transitive_dependencies_phase(
457 &mut self,
458 base_deps: &[(String, ResourceDependency, ResourceType)],
459 enable_transitive: bool,
460 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
461 ) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
462 tracing::info!(
463 "Phase 3: Starting transitive dependency resolution (enable_transitive={})",
464 enable_transitive
465 );
466
467 if enable_transitive {
468 tracing::info!(
469 "Phase 3: Calling resolve_transitive_dependencies with {} base deps",
470 base_deps.len()
471 );
472 let result = self.resolve_transitive_dependencies(base_deps, progress).await?;
473 tracing::info!("Phase 3: Resolved {} total deps (including transitive)", result.len());
474 Ok(result)
475 } else {
476 tracing::info!(
477 "Phase 3: Transitive resolution disabled, using {} base deps",
478 base_deps.len()
479 );
480 Ok(base_deps.to_vec())
481 }
482 }
483
484 /// Phase 4: Resolve each dependency to a locked resource in parallel.
485 ///
486 /// This method processes dependencies in batches for parallelism.
487 async fn resolve_individual_dependencies(
488 &mut self,
489 all_deps: &[(String, ResourceDependency, ResourceType)],
490 lockfile: &mut LockFile,
491 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
492 ) -> Result<()> {
493 let completed_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
494 let total_deps = all_deps.len();
495
496 // Use same default concurrency as version resolution: max(10, 2 × CPU cores)
497 let cores = std::thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
498 let max_concurrent = std::cmp::max(10, cores * 2);
499
500 // Process dependencies in batches to achieve parallelism
501 let mut all_results = Vec::new();
502 for chunk in all_deps.chunks(max_concurrent) {
503 use futures::future::join_all;
504
505 // Clone progress for each batch to avoid closure capture issues
506 let progress_clone = progress.clone();
507
508 // Create futures for this batch by calling async methods directly
509 let batch_futures: Vec<_> = chunk
510 .iter()
511 .map(|(name, dep, resource_type)| {
512 // Build display name for progress tracking
513 let display_name = if dep.get_source().is_some() {
514 if let Some(version) = dep.get_version() {
515 format!("{}@{}", name, version)
516 } else {
517 format!("{}@HEAD", name)
518 }
519 } else {
520 name.clone()
521 };
522 let progress_key = format!("{}:{}", resource_type, &display_name);
523
524 // Mark as active in progress window
525 if let Some(pm) = &progress_clone {
526 pm.mark_item_active(&display_name, &progress_key);
527 }
528
529 // Call the async resolution method directly (returns a Future)
530 let resolution_fut = if dep.is_pattern() {
531 Box::pin(self.resolve_pattern_dependency(name, dep, *resource_type))
532 as std::pin::Pin<
533 Box<
534 dyn std::future::Future<Output = Result<Vec<LockedResource>>>
535 + Send
536 + '_,
537 >,
538 >
539 } else {
540 Box::pin(async {
541 self.resolve_dependency(name, dep, *resource_type)
542 .await
543 .map(|e| vec![e])
544 })
545 as std::pin::Pin<
546 Box<
547 dyn std::future::Future<Output = Result<Vec<LockedResource>>>
548 + Send
549 + '_,
550 >,
551 >
552 };
553
554 (
555 resolution_fut,
556 name.clone(),
557 dep.clone(),
558 *resource_type,
559 progress_key,
560 display_name,
561 )
562 })
563 .collect();
564
565 // Execute all futures in this batch concurrently with timeout
566 let timeout_duration = crate::constants::batch_operation_timeout();
567 let batch_results = tokio::time::timeout(
568 timeout_duration,
569 join_all(batch_futures.into_iter().map(
570 |(fut, name, dep, resource_type, progress_key, display_name)| {
571 let progress_clone = progress_clone.clone();
572 let counter_clone = completed_counter.clone();
573 async move {
574 let result = fut.await;
575
576 // Mark item as complete
577 if let Some(pm) = &progress_clone {
578 let completed = counter_clone
579 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
580 + 1;
581 pm.mark_item_complete(
582 &progress_key,
583 Some(&display_name),
584 completed,
585 total_deps,
586 "Resolving dependencies",
587 );
588 }
589
590 (name, dep, resource_type, result)
591 }
592 },
593 )),
594 )
595 .await
596 .with_context(|| {
597 format!(
598 "Batch dependency resolution timed out after {:?} - possible deadlock",
599 timeout_duration
600 )
601 })?;
602
603 // Collect batch results
604 for (name, dep, resource_type, result) in batch_results {
605 all_results.push(result.map(|entries| (name, dep, resource_type, entries)));
606 }
607 }
608
609 // Process results: track for conflicts and add to lockfile
610 for result in all_results {
611 let (name, dep, resource_type, entries) = result?;
612
613 for entry in entries {
614 // Track for conflict detection using manifest alias
615 // (critical for detecting conflicts between different manifest entries
616 // that resolve to the same canonical resource)
617 self.track_resolved_dependency_for_conflicts(&name, &dep, &entry, resource_type);
618
619 self.add_or_update_lockfile_entry(lockfile, entry);
620 }
621 }
622
623 Ok(())
624 }
625
626 /// Phase 5: Handle conflict detection and backtracking
627 async fn handle_conflicts_and_backtracking(&mut self, lockfile: &mut LockFile) -> Result<()> {
628 // Add resolved dependencies to conflict detector with SHAs and parent metadata
629 tracing::debug!(
630 "Phase 5: Processing {} tracked dependencies for conflict detection",
631 self.resolved_deps_for_conflict_check.len()
632 );
633 for entry in self.resolved_deps_for_conflict_check.iter() {
634 let ((resource_id, required_by, _name), dependency_info) = (entry.key(), entry.value());
635 let ResolvedDependencyInfo {
636 version_constraint,
637 resolved_sha,
638 parent_version,
639 parent_sha,
640 resolution_mode,
641 } = dependency_info;
642
643 // Only add Version path conflicts to the backtracking conflict detector
644 // Git path conflicts are unresolvable and will be handled separately
645 if matches!(resolution_mode, crate::resolver::types::ResolutionMode::Version) {
646 tracing::debug!(
647 "Adding VERSION path to conflict detector: resource_id={}, required_by={}, version={}, sha={}",
648 resource_id,
649 required_by,
650 version_constraint,
651 &resolved_sha[..8.min(resolved_sha.len())]
652 );
653 self.conflict_detector.add_requirement_with_parent(
654 resource_id.clone(),
655 required_by,
656 version_constraint,
657 resolved_sha,
658 parent_version.clone(),
659 parent_sha.clone(),
660 );
661 } else {
662 tracing::debug!(
663 "Skipping GIT path for backtracking: resource_id={}, required_by={}, git_ref={}",
664 resource_id,
665 required_by,
666 version_constraint
667 );
668 }
669 }
670
671 // Phase 5: SHA-based conflict detection
672 let conflict_start = std::time::Instant::now();
673
674 // Populate SHA conflict detector with Git path dependencies only
675 for entry in self.resolved_deps_for_conflict_check.iter() {
676 let ((resource_id, required_by, _name), dependency_info) = (entry.key(), entry.value());
677 let ResolvedDependencyInfo {
678 version_constraint,
679 resolved_sha,
680 parent_version: _parent_version,
681 parent_sha: _parent_sha,
682 resolution_mode,
683 } = dependency_info;
684
685 // Only process Git path conflicts in SHA conflict detector
686 if matches!(resolution_mode, crate::resolver::types::ResolutionMode::GitRef) {
687 // Parse source and path from resource_id
688 let source_str = resource_id.source();
689 let source = source_str.unwrap_or("local");
690 let path = resource_id.name();
691
692 tracing::debug!(
693 "Adding GIT path to SHA conflict detector: source={}, path={}, git_ref={}, sha={}",
694 source,
695 path,
696 version_constraint,
697 &resolved_sha[..8.min(resolved_sha.len())]
698 );
699
700 // Add to SHA conflict detector
701 self.sha_conflict_detector.add_requirement(
702 crate::resolver::sha_conflict_detector::ResolvedRequirement {
703 source: source.to_string(),
704 path: path.to_string(),
705 resolved_sha: resolved_sha.clone(),
706 requested_version: version_constraint.clone(),
707 required_by: required_by.clone(),
708 resolution_mode: *resolution_mode,
709 },
710 );
711 }
712 }
713
714 // Detect SHA conflicts
715 let sha_conflicts = self.sha_conflict_detector.detect_conflicts()?;
716 let conflict_detect_duration = conflict_start.elapsed();
717 tracing::debug!(
718 "Phase 5: SHA conflict detection took {:?} for {} tracked dependencies",
719 conflict_detect_duration,
720 self.resolved_deps_for_conflict_check.len()
721 );
722
723 if !sha_conflicts.is_empty() {
724 // SHA conflicts are true conflicts that cannot be resolved by backtracking
725 // Report them as errors
726 let error_messages: Vec<String> =
727 sha_conflicts.iter().map(|conflict| conflict.format_error()).collect();
728
729 return Err(anyhow::anyhow!(
730 "Unresolvable SHA conflicts detected:\n{}",
731 error_messages.join("\n")
732 ));
733 }
734
735 // Phase 6: Version-based conflict detection (only for Version path dependencies)
736 // Use the original conflict detector for version constraint conflicts
737 let conflicts = self.conflict_detector.detect_conflicts();
738
739 if !conflicts.is_empty() {
740 tracing::info!(
741 "Detected {} version constraint conflict(s), attempting automatic resolution...",
742 conflicts.len()
743 );
744
745 // Attempt backtracking to find compatible versions
746 let mut backtracker =
747 backtracking::BacktrackingResolver::new(&self.core, &mut self.version_service);
748
749 // Populate the backtracker's resource registry from the conflict detector
750 // This provides the complete dependency graph for conflict detection during backtracking
751 backtracker.populate_from_conflict_detector(&self.conflict_detector);
752
753 match backtracker.resolve_conflicts(&conflicts).await {
754 Ok(result) if result.resolved => {
755 // Log success with all metrics
756 if result.total_transitive_reresolutions > 0 {
757 tracing::info!(
758 "✓ Resolved conflicts after {} iteration(s): {} version(s) adjusted, {} transitive re-resolution(s)",
759 result.iterations,
760 result.updates.len(),
761 result.total_transitive_reresolutions
762 );
763 } else {
764 tracing::info!(
765 "✓ Resolved conflicts after {} iteration(s): {} version(s) adjusted",
766 result.iterations,
767 result.updates.len()
768 );
769 }
770
771 // Log what changed
772 for update in &result.updates {
773 tracing::info!(
774 " {} : {} → {}",
775 update.resource_id,
776 update.old_version,
777 update.new_version
778 );
779 }
780
781 // Apply the backtracking updates to prepared versions
782 self.apply_backtracking_updates(&result.updates).await?;
783
784 // Update lockfile entries with new SHAs and paths
785 self.update_lockfile_entries(lockfile, &result.updates)?;
786
787 tracing::info!("Applied backtracking updates, backtracking complete");
788 }
789 Ok(result) => {
790 // Backtracking failed - log the reason
791 let reason_msg = match result.termination_reason {
792 backtracking::TerminationReason::MaxIterations => {
793 format!("reached max iterations ({})", result.iterations)
794 }
795 backtracking::TerminationReason::Timeout => "timeout exceeded".to_string(),
796 backtracking::TerminationReason::NoProgress => {
797 "no progress made (same conflicts persist)".to_string()
798 }
799 backtracking::TerminationReason::Oscillation => {
800 "oscillation detected (cycling between conflict states)".to_string()
801 }
802 backtracking::TerminationReason::NoCompatibleVersion => {
803 "no compatible version found".to_string()
804 }
805 _ => "unknown reason".to_string(),
806 };
807
808 tracing::warn!("Backtracking failed: {}", reason_msg);
809
810 // Use original error with detailed conflict information
811 let mut error_msg = format!(
812 "Version conflicts detected (automatic resolution failed: {}):\n\n",
813 reason_msg
814 );
815 for conflict in &conflicts {
816 error_msg.push_str(&format!("{conflict}\n"));
817 }
818 error_msg.push_str(
819 "\nSuggestion: Manually specify compatible versions in agpm.toml",
820 );
821 return Err(anyhow::anyhow!("{}", error_msg));
822 }
823 Err(e) => {
824 // Backtracking encountered an error
825 tracing::error!("Backtracking error: {}", e);
826 let mut error_msg = format!(
827 "Version conflicts detected (automatic resolution error: {}):\n\n",
828 e
829 );
830 for conflict in &conflicts {
831 error_msg.push_str(&format!("{conflict}\n"));
832 }
833 return Err(anyhow::anyhow!("{}", error_msg));
834 }
835 }
836 }
837
838 Ok(())
839 }
840
841 /// Phase 6: Final post-processing and cleanup
842 fn finalize_resolution(
843 &mut self,
844 lockfile: &mut LockFile,
845 progress: &Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
846 ) -> Result<()> {
847 // Post-process dependencies and detect target conflicts
848 self.add_version_to_dependencies(lockfile)?;
849 self.detect_target_conflicts(lockfile)?;
850
851 // Complete the resolution phase (includes all phases: version resolution,
852 // transitive deps, conflict detection)
853 if let Some(pm) = progress {
854 let total_resources = lockfile.agents.len()
855 + lockfile.commands.len()
856 + lockfile.scripts.len()
857 + lockfile.hooks.len()
858 + lockfile.snippets.len()
859 + lockfile.mcp_servers.len()
860 + lockfile.skills.len();
861 pm.complete_phase_with_window(Some(&format!(
862 "Resolved {} dependencies",
863 total_resources
864 )));
865 }
866
867 Ok(())
868 }
869
870 /// Pre-sync sources for the given dependencies.
871 ///
872 /// This performs Git operations to ensure all required sources are available
873 /// before the main resolution process begins.
874 ///
875 /// # Arguments
876 ///
877 /// * `deps` - List of (name, dependency) pairs to sync sources for
878 ///
879 /// # Errors
880 ///
881 /// Returns an error if source synchronization fails
882 pub async fn pre_sync_sources(
883 &mut self,
884 deps: &[(String, ResourceDependency)],
885 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
886 ) -> Result<()> {
887 // Pre-sync all sources using version service
888 self.version_service.pre_sync_sources(&self.core, deps, progress).await?;
889 self.sources_pre_synced.store(true, std::sync::atomic::Ordering::Release);
890 Ok(())
891 }
892
893 /// Update dependencies with existing lockfile and specific dependencies to update.
894 ///
895 /// # Arguments
896 ///
897 /// * `existing` - Existing lockfile to update
898 /// * `deps_to_update` - Optional specific dependency names to update (None = all)
899 /// * `progress` - Optional multi-phase progress tracker for UI updates
900 ///
901 /// # Current Implementation (MVP)
902 ///
903 /// Currently performs full resolution regardless of `deps_to_update` value.
904 /// This is correct but not optimized - all dependencies are re-resolved even
905 /// when only specific ones are requested.
906 ///
907 /// # Future Enhancement
908 ///
909 /// Full incremental update would:
910 /// 1. Match `deps_to_update` names to lockfile entries
911 /// 2. Keep unchanged dependencies at their locked versions
912 /// 3. Re-resolve only specified dependencies to latest matching versions
913 /// 4. Re-extract transitive dependencies for updated resources
914 /// 5. Merge updated entries with unchanged entries from existing lockfile
915 /// 6. Detect and resolve any new conflicts
916 ///
917 /// This requires significant changes to the resolution pipeline to support
918 /// "pinned" versions alongside "latest" resolution.
919 ///
920 /// # Errors
921 ///
922 /// Returns an error if update process fails
923 pub async fn update(
924 &mut self,
925 existing: &LockFile,
926 deps_to_update: Option<Vec<String>>,
927 progress: Option<Arc<crate::utils::MultiPhaseProgress>>,
928 ) -> Result<LockFile> {
929 match deps_to_update {
930 None => {
931 // Update all dependencies (full resolution)
932 tracing::debug!("Performing full resolution for all dependencies");
933 self.resolve_with_options(true, progress).await
934 }
935 Some(names) => {
936 // Incremental update requested
937 tracing::debug!("Incremental update requested for: {:?}", names);
938
939 // Phase 1: Filter lockfile entries into unchanged and to-update
940 let (unchanged, to_resolve) = Self::filter_lockfile_entries(existing, &names);
941
942 if to_resolve.is_empty() {
943 tracing::warn!("No matching dependencies found in lockfile: {:?}", names);
944 return Ok(existing.clone());
945 }
946
947 tracing::debug!(
948 "Resolving {} dependencies, keeping {} unchanged",
949 to_resolve.len(),
950 unchanged.agents.len()
951 + unchanged.snippets.len()
952 + unchanged.commands.len()
953 + unchanged.scripts.len()
954 + unchanged.hooks.len()
955 + unchanged.mcp_servers.len()
956 + unchanged.skills.len()
957 );
958
959 // Phase 2: Create filtered manifest with only deps to update
960 let filtered_manifest = self.create_filtered_manifest(&to_resolve);
961
962 // Phase 3: Create temporary resolver for filtered manifest
963 // Re-use the same cache and operation context
964 let mut temp_resolver = DependencyResolver::new_with_context(
965 filtered_manifest,
966 self.core.cache().clone(),
967 self.core.operation_context().cloned(),
968 )
969 .await?;
970
971 // Phase 4: Resolve filtered dependencies with updates allowed
972 let updated = temp_resolver.resolve_with_options(true, progress).await?;
973
974 // Phase 5: Merge unchanged and updated lockfiles
975 let merged = Self::merge_lockfiles(unchanged, updated);
976
977 tracing::debug!(
978 "Incremental update complete: merged lockfile has {} total entries",
979 merged.agents.len()
980 + merged.snippets.len()
981 + merged.commands.len()
982 + merged.scripts.len()
983 + merged.hooks.len()
984 + merged.mcp_servers.len()
985 + merged.skills.len()
986 );
987
988 Ok(merged)
989 }
990 }
991 }
992
993 /// Get available versions for a repository.
994 ///
995 /// # Arguments
996 ///
997 /// * `repo_path` - Path to the Git repository
998 ///
999 /// # Returns
1000 ///
1001 /// List of available version strings (tags and branches)
1002 pub async fn get_available_versions(&self, repo_path: &Path) -> Result<Vec<String>> {
1003 VersionResolutionService::get_available_versions(&self.core, repo_path).await
1004 }
1005
1006 /// Verify that existing lockfile is still valid.
1007 ///
1008 /// # Arguments
1009 ///
1010 /// * `_lockfile` - Existing lockfile to verify
1011 ///
1012 /// # Errors
1013 ///
1014 /// Returns an error if verification fails
1015 pub async fn verify(&self, _lockfile: &LockFile) -> Result<()> {
1016 // TODO: Implement verification logic using services
1017 Ok(())
1018 }
1019
1020 /// Get current operation context if available.
1021 pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
1022 self.core.operation_context()
1023 }
1024
1025 /// Set the operation context for warning deduplication.
1026 ///
1027 /// # Arguments
1028 ///
1029 /// * `context` - The operation context to use
1030 pub fn set_operation_context(&mut self, context: Arc<OperationContext>) {
1031 self.core.operation_context = Some(context);
1032 }
1033}
1034
1035// Private helper methods
1036impl DependencyResolver {
1037 /// Build an index of manifest overrides for deduplication with transitive deps.
1038 ///
1039 /// This method creates a mapping from resource identity (source, path, tool, variant_hash)
1040 /// to the customizations (filename, target, install, template_vars) specified in the manifest.
1041 /// When a transitive dependency is discovered that matches a manifest dependency, the manifest
1042 /// version's customizations will take precedence.
1043 fn build_manifest_override_index(
1044 &self,
1045 base_deps: &[(String, ResourceDependency, ResourceType)],
1046 ) -> types::ManifestOverrideIndex {
1047 use crate::resolver::types::{ManifestOverride, OverrideKey, normalize_lookup_path};
1048
1049 let mut index = HashMap::new();
1050
1051 for (name, dep, resource_type) in base_deps {
1052 // Skip pattern dependencies (they expand later)
1053 if dep.is_pattern() {
1054 continue;
1055 }
1056
1057 // Build the override key
1058 let normalized_path = normalize_lookup_path(dep.get_path());
1059 let source = dep.get_source().map(std::string::ToString::to_string);
1060
1061 // Determine tool for this dependency
1062 let tool = dep
1063 .get_tool()
1064 .map(str::to_string)
1065 .unwrap_or_else(|| self.core.manifest().get_default_tool(*resource_type));
1066
1067 // Compute variant_hash from MERGED variant_inputs (dep + global config)
1068 // This ensures manifest overrides use the same hash as LockedResources
1069 let merged_variant_inputs =
1070 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep);
1071 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
1072 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
1073
1074 let key = OverrideKey {
1075 resource_type: *resource_type,
1076 normalized_path,
1077 source,
1078 tool,
1079 variant_hash,
1080 };
1081
1082 // Build the override info
1083 let override_info = ManifestOverride {
1084 filename: dep.get_filename().map(std::string::ToString::to_string),
1085 target: dep.get_target().map(std::string::ToString::to_string),
1086 install: dep.get_install(),
1087 manifest_alias: Some(name.clone()),
1088 template_vars: dep.get_template_vars().cloned(),
1089 };
1090
1091 tracing::debug!(
1092 "Adding manifest override for {:?}:{} (tool={}, variant_hash={})",
1093 resource_type,
1094 dep.get_path(),
1095 key.tool,
1096 key.variant_hash
1097 );
1098
1099 index.insert(key, override_info);
1100 }
1101
1102 tracing::info!("Built manifest override index with {} entries", index.len());
1103 index
1104 }
1105
1106 /// Resolve transitive dependencies starting from base dependencies.
1107 ///
1108 /// Discovers dependencies declared in resource files, expands patterns,
1109 /// builds dependency graph with cycle detection, and returns all dependencies
1110 /// in topological order.
1111 async fn resolve_transitive_dependencies(
1112 &mut self,
1113 base_deps: &[(String, ResourceDependency, ResourceType)],
1114 progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
1115 ) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
1116 use crate::resolver::transitive_resolver;
1117
1118 // Build override index FIRST from manifest dependencies
1119 let manifest_overrides = self.build_manifest_override_index(base_deps);
1120
1121 // Build ResolutionContext for the transitive resolver
1122 let resolution_ctx = ResolutionContext {
1123 manifest: self.core.manifest(),
1124 cache: self.core.cache(),
1125 source_manager: self.core.source_manager(),
1126 operation_context: self.core.operation_context(),
1127 };
1128
1129 // Build TransitiveContext with concurrent state and the override index
1130 let mut ctx = TransitiveContext {
1131 base: resolution_ctx,
1132 dependency_map: &self.dependency_map,
1133 transitive_custom_names: &self.transitive_custom_names,
1134 conflict_detector: &mut self.conflict_detector,
1135 manifest_overrides: &manifest_overrides,
1136 };
1137
1138 // Get prepared versions from version service (clone Arc for shared access)
1139 // Use prepared_versions_ready_arc to get only Ready versions, filtering out Preparing states
1140 let prepared_versions = self.version_service.prepared_versions_ready_arc();
1141
1142 // Create services container
1143 let services = transitive_resolver::ResolutionServices {
1144 version_service: &self.version_service,
1145 pattern_service: &self.pattern_service,
1146 };
1147
1148 // Call the service-based transitive resolver
1149 transitive_resolver::resolve_with_services(
1150 transitive_resolver::TransitiveResolutionParams {
1151 ctx: &mut ctx,
1152 core: &self.core,
1153 base_deps,
1154 enable_transitive: true,
1155 prepared_versions: &prepared_versions,
1156 pattern_alias_map: &self.pattern_alias_map,
1157 services: &services,
1158 progress,
1159 },
1160 )
1161 .await
1162 }
1163
1164 /// Get the list of transitive dependencies for a resource.
1165 ///
1166 /// Returns the dependency IDs (format: "type/name") for all transitive
1167 /// dependencies discovered during resolution.
1168 fn get_dependencies_for(
1169 &self,
1170 name: &str,
1171 source: Option<&str>,
1172 resource_type: ResourceType,
1173 tool: Option<&str>,
1174 variant_hash: &str,
1175 ) -> Vec<String> {
1176 let key = (
1177 resource_type,
1178 name.to_string(),
1179 source.map(std::string::ToString::to_string),
1180 tool.map(std::string::ToString::to_string),
1181 variant_hash.to_string(),
1182 );
1183 let result = self.dependency_map.get(&key).map(|v| v.clone()).unwrap_or_default();
1184 tracing::debug!(
1185 "[DEBUG] get_dependencies_for: name='{}', type={:?}, source={:?}, tool={:?}, hash={}, found={} deps",
1186 name,
1187 resource_type,
1188 source,
1189 tool,
1190 &variant_hash[..8],
1191 result.len()
1192 );
1193 result
1194 }
1195
1196 /// Get pattern alias for a concrete dependency.
1197 ///
1198 /// Returns the pattern name if this dependency was created from a pattern expansion.
1199 fn get_pattern_alias_for_dependency(
1200 &self,
1201 name: &str,
1202 resource_type: ResourceType,
1203 ) -> Option<String> {
1204 // Check if this dependency was created from a pattern expansion
1205 self.pattern_alias_map.get(&(resource_type, name.to_string())).map(|v| v.clone())
1206 }
1207}
1208
1209#[cfg(test)]
1210mod resolver_tests {
1211 use super::*;
1212
1213 #[tokio::test]
1214 async fn test_resolver_creation() -> Result<()> {
1215 let manifest = Manifest::default();
1216 let cache = Cache::new()?;
1217 DependencyResolver::new(manifest, cache).await?;
1218 Ok(())
1219 }
1220
1221 #[tokio::test]
1222 async fn test_resolver_with_global() -> Result<()> {
1223 let manifest = Manifest::default();
1224 let cache = Cache::new()?;
1225 DependencyResolver::new_with_global(manifest, cache).await?;
1226 Ok(())
1227 }
1228}