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}