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 conflict_service;
20pub mod dependency_graph;
21pub mod lockfile_builder;
22pub mod path_resolver;
23pub mod pattern_expander;
24pub mod resource_service;
25pub mod source_context;
26pub mod transitive_resolver;
27pub mod types;
28pub mod version_resolver;
29
30// Re-export utility functions for compatibility
31pub use path_resolver::{extract_meaningful_path, is_file_relative_path, normalize_bare_filename};
32
33use std::collections::HashMap;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36
37use anyhow::Result;
38
39use crate::cache::Cache;
40use crate::core::{OperationContext, ResourceType};
41use crate::lockfile::{LockFile, LockedResource};
42use crate::manifest::{Manifest, ResourceDependency};
43use crate::source::SourceManager;
44
45// Re-export services for external use
46pub use conflict_service::ConflictService;
47pub use pattern_expander::PatternExpansionService;
48pub use resource_service::ResourceFetchingService;
49pub use types::ResolutionCore;
50pub use version_resolver::{
51    VersionResolutionService, VersionResolver as VersionResolverExport, find_best_matching_tag,
52    is_version_constraint, parse_tags_to_versions,
53};
54
55// Legacy re-exports for compatibility
56pub use dependency_graph::{DependencyGraph, DependencyNode};
57pub use lockfile_builder::LockfileBuilder;
58pub use pattern_expander::{expand_pattern_to_concrete_deps, generate_dependency_name};
59pub use types::{
60    DependencyKey, ManifestOverride, ManifestOverrideIndex, OverrideKey, ResolutionContext,
61    TransitiveContext,
62};
63
64pub use version_resolver::{PreparedSourceVersion, VersionResolver, WorktreeManager};
65
66/// Main dependency resolver with service-based architecture.
67///
68/// This orchestrates multiple specialized services to handle different aspects
69/// of the dependency resolution process while maintaining compatibility
70/// with existing interfaces.
71#[allow(dead_code)] // Some fields not yet used in service-based refactoring
72pub struct DependencyResolver {
73    /// Core shared context with immutable state
74    core: ResolutionCore,
75
76    /// Version resolution and Git operations service
77    version_service: VersionResolutionService,
78
79    /// Pattern expansion service for glob dependencies
80    pattern_service: PatternExpansionService,
81
82    /// Conflict detection service
83    conflict_service: ConflictService,
84
85    /// Resource fetching and metadata service
86    resource_service: ResourceFetchingService,
87
88    /// Conflict detector for version conflicts
89    conflict_detector: crate::version::conflict::ConflictDetector,
90
91    /// Dependency tracking state
92    dependency_map: HashMap<DependencyKey, Vec<String>>,
93
94    /// Pattern alias tracking for expanded patterns
95    pattern_alias_map: HashMap<(ResourceType, String), String>,
96
97    /// Transitive dependency custom names
98    transitive_custom_names: HashMap<DependencyKey, String>,
99
100    /// Track if sources have been pre-synced to avoid duplicate work
101    sources_pre_synced: bool,
102}
103
104impl DependencyResolver {
105    /// Create a new dependency resolver.
106    ///
107    /// # Arguments
108    ///
109    /// * `manifest` - Project manifest with dependencies
110    /// * `cache` - Cache for Git operations and worktrees
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if source manager cannot be created
115    pub async fn new(manifest: Manifest, cache: Cache) -> Result<Self> {
116        Self::new_with_context(manifest, cache, None).await
117    }
118
119    /// Create a new dependency resolver with operation context.
120    ///
121    /// # Arguments
122    ///
123    /// * `manifest` - Project manifest with dependencies
124    /// * `cache` - Cache for Git operations and worktrees
125    /// * `operation_context` - Optional context for warning deduplication
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if source manager cannot be created
130    pub async fn new_with_context(
131        manifest: Manifest,
132        cache: Cache,
133        operation_context: Option<Arc<OperationContext>>,
134    ) -> Result<Self> {
135        // Create source manager from manifest
136        let source_manager = SourceManager::from_manifest(&manifest)?;
137
138        // Create resolution core with shared state
139        let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
140
141        // Initialize all services
142        let version_service = VersionResolutionService::new(core.cache().clone());
143        let pattern_service = PatternExpansionService::new();
144        let conflict_service = ConflictService::new();
145        let resource_service = ResourceFetchingService::new();
146
147        Ok(Self {
148            core,
149            version_service,
150            pattern_service,
151            conflict_service,
152            resource_service,
153            conflict_detector: crate::version::conflict::ConflictDetector::new(),
154            dependency_map: HashMap::new(),
155            pattern_alias_map: HashMap::new(),
156            transitive_custom_names: HashMap::new(),
157            sources_pre_synced: false,
158        })
159    }
160
161    /// Create a new resolver with global configuration support.
162    ///
163    /// This loads both manifest sources and global sources from `~/.agpm/config.toml`.
164    ///
165    /// # Arguments
166    ///
167    /// * `manifest` - Project manifest with dependencies
168    /// * `cache` - Cache for Git operations and worktrees
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if global configuration cannot be loaded
173    pub async fn new_with_global(manifest: Manifest, cache: Cache) -> Result<Self> {
174        Self::new_with_global_context(manifest, cache, None).await
175    }
176
177    /// Creates a new dependency resolver with custom cache directory.
178    ///
179    /// # Arguments
180    ///
181    /// * `cache` - Cache for Git operations and worktrees
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if source manager cannot be created
186    pub async fn with_cache(manifest: Manifest, cache: Cache) -> Result<Self> {
187        Self::new_with_context(manifest, cache, None).await
188    }
189
190    /// Create a new resolver with global configuration and operation context.
191    ///
192    /// This loads both manifest sources and global sources from `~/.agpm/config.toml`.
193    ///
194    /// # Arguments
195    ///
196    /// * `manifest` - Project manifest with dependencies
197    /// * `cache` - Cache for Git operations and worktrees
198    /// * `operation_context` - Optional context for warning deduplication
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if global configuration cannot be loaded
203    pub async fn new_with_global_context(
204        manifest: Manifest,
205        cache: Cache,
206        _operation_context: Option<Arc<OperationContext>>,
207    ) -> Result<Self> {
208        let source_manager = SourceManager::from_manifest_with_global(&manifest).await?;
209
210        let core = ResolutionCore::new(manifest, cache, source_manager, _operation_context);
211
212        let version_service = VersionResolutionService::new(core.cache().clone());
213        let pattern_service = PatternExpansionService::new();
214        let conflict_service = ConflictService::new();
215        let resource_service = ResourceFetchingService::new();
216
217        Ok(Self {
218            core,
219            version_service,
220            pattern_service,
221            conflict_service,
222            resource_service,
223            conflict_detector: crate::version::conflict::ConflictDetector::new(),
224            dependency_map: HashMap::new(),
225            pattern_alias_map: HashMap::new(),
226            transitive_custom_names: HashMap::new(),
227            sources_pre_synced: false,
228        })
229    }
230
231    /// Get a reference to the resolution core.
232    pub fn core(&self) -> &ResolutionCore {
233        &self.core
234    }
235
236    /// Resolve all dependencies and generate a complete lockfile.
237    ///
238    /// This is the main resolution method.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if any step of resolution fails
243    pub async fn resolve(&mut self) -> Result<LockFile> {
244        self.resolve_with_options(true).await
245    }
246
247    /// Resolve dependencies with transitive resolution option.
248    ///
249    /// # Arguments
250    ///
251    /// * `enable_transitive` - Whether to resolve transitive dependencies
252    ///
253    /// # Errors
254    ///
255    /// Returns an error if resolution fails
256    pub async fn resolve_with_options(&mut self, enable_transitive: bool) -> Result<LockFile> {
257        let mut lockfile = LockFile::new();
258
259        // Add sources to lockfile
260        for (name, url) in &self.core.manifest().sources {
261            lockfile.add_source(name.clone(), url.clone(), String::new());
262        }
263
264        // Phase 1: Extract dependencies from manifest with types
265        let base_deps: Vec<(String, ResourceDependency, ResourceType)> = self
266            .core
267            .manifest()
268            .all_dependencies_with_types()
269            .into_iter()
270            .map(|(name, dep, resource_type)| (name.to_string(), dep.into_owned(), resource_type))
271            .collect();
272
273        // Add direct dependencies to conflict detector
274        for (name, dep, _) in &base_deps {
275            self.add_to_conflict_detector(name, dep, "manifest");
276        }
277
278        // Phase 2: Pre-sync all sources if not already done
279        if !self.sources_pre_synced {
280            let deps_for_sync: Vec<(String, ResourceDependency)> =
281                base_deps.iter().map(|(name, dep, _)| (name.clone(), dep.clone())).collect();
282            self.version_service.pre_sync_sources(&self.core, &deps_for_sync).await?;
283            self.sources_pre_synced = true;
284        }
285
286        // Phase 3: Resolve transitive dependencies
287        let all_deps = if enable_transitive {
288            self.resolve_transitive_dependencies(&base_deps).await?
289        } else {
290            base_deps.clone()
291        };
292
293        // Phase 4: Resolve each dependency to a locked resource
294        for (name, dep, resource_type) in &all_deps {
295            if dep.is_pattern() {
296                // Pattern dependencies resolve to multiple resources
297                let entries = self.resolve_pattern_dependency(name, dep, *resource_type).await?;
298
299                // Add each resolved entry with deduplication
300                for entry in entries {
301                    let entry_name = entry.name.clone();
302                    self.add_or_update_lockfile_entry(&mut lockfile, &entry_name, entry);
303                }
304            } else {
305                // Regular single dependency
306                let entry = self.resolve_dependency(name, dep, *resource_type).await?;
307                self.add_or_update_lockfile_entry(&mut lockfile, name, entry);
308            }
309        }
310
311        // Phase 5: Detect conflicts
312        let conflicts = self.conflict_detector.detect_conflicts();
313        if !conflicts.is_empty() {
314            let mut error_msg = String::from("Version conflicts detected:\n\n");
315            for conflict in &conflicts {
316                error_msg.push_str(&format!("{conflict}\n"));
317            }
318            return Err(anyhow::anyhow!("{}", error_msg));
319        }
320
321        // Phase 6: Post-process dependencies and detect target conflicts
322        self.add_version_to_dependencies(&mut lockfile)?;
323        self.detect_target_conflicts(&lockfile)?;
324
325        Ok(lockfile)
326    }
327
328    /// Pre-sync sources for the given dependencies.
329    ///
330    /// This performs Git operations to ensure all required sources are available
331    /// before the main resolution process begins.
332    ///
333    /// # Arguments
334    ///
335    /// * `deps` - List of (name, dependency) pairs to sync sources for
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if source synchronization fails
340    pub async fn pre_sync_sources(&mut self, deps: &[(String, ResourceDependency)]) -> Result<()> {
341        // Pre-sync all sources using version service
342        self.version_service.pre_sync_sources(&self.core, deps).await?;
343        self.sources_pre_synced = true;
344        Ok(())
345    }
346
347    /// Update dependencies with existing lockfile and specific dependencies to update.
348    ///
349    /// # Arguments
350    ///
351    /// * `existing` - Existing lockfile to update
352    /// * `deps_to_update` - Optional specific dependency names to update (None = all)
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if update process fails
357    pub async fn update(
358        &mut self,
359        existing: &LockFile,
360        deps_to_update: Option<Vec<String>>,
361    ) -> Result<LockFile> {
362        // For now, just resolve all dependencies
363        // TODO: Implement proper incremental update logic using deps_to_update names
364        let _existing = existing; // Suppress unused warning for now
365        let _deps_to_update = deps_to_update; // Suppress unused warning for now
366        self.resolve_with_options(true).await
367    }
368
369    /// Get available versions for a repository.
370    ///
371    /// # Arguments
372    ///
373    /// * `repo_path` - Path to the Git repository
374    ///
375    /// # Returns
376    ///
377    /// List of available version strings (tags and branches)
378    pub async fn get_available_versions(&self, repo_path: &Path) -> Result<Vec<String>> {
379        VersionResolutionService::get_available_versions(&self.core, repo_path).await
380    }
381
382    /// Verify that existing lockfile is still valid.
383    ///
384    /// # Arguments
385    ///
386    /// * `_lockfile` - Existing lockfile to verify
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if verification fails
391    pub async fn verify(&self, _lockfile: &LockFile) -> Result<()> {
392        // TODO: Implement verification logic using services
393        Ok(())
394    }
395
396    /// Get current operation context if available.
397    pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
398        self.core.operation_context()
399    }
400
401    /// Set the operation context for warning deduplication.
402    ///
403    /// # Arguments
404    ///
405    /// * `context` - The operation context to use
406    pub fn set_operation_context(&mut self, context: Arc<OperationContext>) {
407        self.core.operation_context = Some(context);
408    }
409}
410
411// Private helper methods
412impl DependencyResolver {
413    /// Build an index of manifest overrides for deduplication with transitive deps.
414    ///
415    /// This method creates a mapping from resource identity (source, path, tool, variant_hash)
416    /// to the customizations (filename, target, install, template_vars) specified in the manifest.
417    /// When a transitive dependency is discovered that matches a manifest dependency, the manifest
418    /// version's customizations will take precedence.
419    fn build_manifest_override_index(
420        &self,
421        base_deps: &[(String, ResourceDependency, ResourceType)],
422    ) -> types::ManifestOverrideIndex {
423        use crate::resolver::types::{ManifestOverride, OverrideKey, normalize_lookup_path};
424
425        let mut index = HashMap::new();
426
427        for (name, dep, resource_type) in base_deps {
428            // Skip pattern dependencies (they expand later)
429            if dep.is_pattern() {
430                continue;
431            }
432
433            // Build the override key
434            let normalized_path = normalize_lookup_path(dep.get_path());
435            let source = dep.get_source().map(std::string::ToString::to_string);
436
437            // Determine tool for this dependency
438            let tool = dep
439                .get_tool()
440                .map(str::to_string)
441                .unwrap_or_else(|| self.core.manifest().get_default_tool(*resource_type));
442
443            // Compute variant_hash from MERGED variant_inputs (dep + global config)
444            // This ensures manifest overrides use the same hash as LockedResources
445            let merged_variant_inputs =
446                lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep);
447            let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
448                .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
449
450            let key = OverrideKey {
451                resource_type: *resource_type,
452                normalized_path,
453                source,
454                tool,
455                variant_hash,
456            };
457
458            // Build the override info
459            let override_info = ManifestOverride {
460                filename: dep.get_filename().map(std::string::ToString::to_string),
461                target: dep.get_target().map(std::string::ToString::to_string),
462                install: dep.get_install(),
463                manifest_alias: Some(name.clone()),
464                template_vars: dep.get_template_vars().cloned(),
465            };
466
467            tracing::debug!(
468                "Adding manifest override for {:?}:{} (tool={}, variant_hash={})",
469                resource_type,
470                dep.get_path(),
471                key.tool,
472                key.variant_hash
473            );
474
475            index.insert(key, override_info);
476        }
477
478        tracing::info!("Built manifest override index with {} entries", index.len());
479        index
480    }
481
482    /// Resolve transitive dependencies starting from base dependencies.
483    ///
484    /// Discovers dependencies declared in resource files, expands patterns,
485    /// builds dependency graph with cycle detection, and returns all dependencies
486    /// in topological order.
487    async fn resolve_transitive_dependencies(
488        &mut self,
489        base_deps: &[(String, ResourceDependency, ResourceType)],
490    ) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
491        use crate::resolver::transitive_resolver;
492
493        // Build override index FIRST from manifest dependencies
494        let manifest_overrides = self.build_manifest_override_index(base_deps);
495
496        // Build ResolutionContext for the transitive resolver
497        let resolution_ctx = ResolutionContext {
498            manifest: self.core.manifest(),
499            cache: self.core.cache(),
500            source_manager: self.core.source_manager(),
501            operation_context: self.core.operation_context(),
502        };
503
504        // Build TransitiveContext with mutable state and the override index
505        let mut ctx = TransitiveContext {
506            base: resolution_ctx,
507            dependency_map: &mut self.dependency_map,
508            transitive_custom_names: &mut self.transitive_custom_names,
509            conflict_detector: &mut self.conflict_detector,
510            manifest_overrides: &manifest_overrides,
511        };
512
513        // Get prepared versions from version service (clone to avoid borrow conflicts)
514        let prepared_versions = self.version_service.prepared_versions().clone();
515
516        // Create services container
517        let mut services = transitive_resolver::ResolutionServices {
518            version_service: &mut self.version_service,
519            pattern_service: &mut self.pattern_service,
520        };
521
522        // Call the service-based transitive resolver
523        transitive_resolver::resolve_with_services(
524            &mut ctx,
525            &self.core,
526            base_deps,
527            true, // enable_transitive
528            &prepared_versions,
529            &mut self.pattern_alias_map,
530            &mut services,
531        )
532        .await
533    }
534
535    /// Get the list of transitive dependencies for a resource.
536    ///
537    /// Returns the dependency IDs (format: "type/name") for all transitive
538    /// dependencies discovered during resolution.
539    fn get_dependencies_for(
540        &self,
541        name: &str,
542        source: Option<&str>,
543        resource_type: ResourceType,
544        tool: Option<&str>,
545        variant_hash: &str,
546    ) -> Vec<String> {
547        let key = (
548            resource_type,
549            name.to_string(),
550            source.map(std::string::ToString::to_string),
551            tool.map(std::string::ToString::to_string),
552            variant_hash.to_string(),
553        );
554        let result = self.dependency_map.get(&key).cloned().unwrap_or_default();
555        tracing::debug!(
556            "[DEBUG] get_dependencies_for: name='{}', type={:?}, source={:?}, tool={:?}, hash={}, found={} deps",
557            name,
558            resource_type,
559            source,
560            tool,
561            &variant_hash[..8],
562            result.len()
563        );
564        result
565    }
566
567    /// Get pattern alias for a concrete dependency.
568    ///
569    /// Returns the pattern name if this dependency was created from a pattern expansion.
570    fn get_pattern_alias_for_dependency(
571        &self,
572        name: &str,
573        resource_type: ResourceType,
574    ) -> Option<String> {
575        // Check if this dependency was created from a pattern expansion
576        self.pattern_alias_map.get(&(resource_type, name.to_string())).cloned()
577    }
578
579    /// Resolve a single dependency to a lockfile entry.
580    ///
581    /// Handles both local and remote dependencies, computing proper installation
582    /// paths and including all necessary metadata.
583    async fn resolve_dependency(
584        &mut self,
585        name: &str,
586        dep: &ResourceDependency,
587        resource_type: ResourceType,
588    ) -> Result<LockedResource> {
589        use crate::resolver::lockfile_builder;
590        use crate::resolver::path_resolver as install_path_resolver;
591        use crate::utils::normalize_path_for_storage;
592
593        tracing::debug!(
594            "resolve_dependency: name={}, path={}, source={:?}, is_local={}",
595            name,
596            dep.get_path(),
597            dep.get_source(),
598            dep.is_local()
599        );
600
601        if dep.is_local() {
602            // Local dependency
603            let filename = if let Some(custom_filename) = dep.get_filename() {
604                custom_filename.to_string()
605            } else {
606                extract_meaningful_path(Path::new(dep.get_path()))
607            };
608
609            let artifact_type_string = dep
610                .get_tool()
611                .map(|s| s.to_string())
612                .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
613            let artifact_type = artifact_type_string.as_str();
614
615            let installed_at = install_path_resolver::resolve_install_path(
616                self.core.manifest(),
617                dep,
618                artifact_type,
619                resource_type,
620                &filename,
621            )?;
622
623            // Determine manifest_alias: only set for direct manifest dependencies or pattern-expanded
624            let has_pattern_alias = self.get_pattern_alias_for_dependency(name, resource_type);
625            let is_in_manifest = self
626                .core
627                .manifest()
628                .get_dependencies(resource_type)
629                .is_some_and(|deps| deps.contains_key(name));
630
631            let manifest_alias = if let Some(ref pattern_alias) = has_pattern_alias {
632                // Pattern-expanded dependency - use pattern name as manifest_alias
633                Some(pattern_alias.clone())
634            } else if is_in_manifest {
635                // Direct manifest dependency - use name as manifest_alias
636                Some(name.to_string())
637            } else {
638                // Transitive dependency - no manifest_alias
639                None
640            };
641
642            tracing::debug!(
643                "manifest_alias calculation: name={}, path={}, has_pattern_alias={}, is_in_manifest={}, manifest_alias={:?}",
644                name,
645                dep.get_path(),
646                has_pattern_alias.is_some(),
647                is_in_manifest,
648                manifest_alias
649            );
650
651            let applied_patches = lockfile_builder::get_patches_for_resource(
652                self.core.manifest(),
653                resource_type,
654                name,
655                manifest_alias.as_deref(),
656            );
657
658            // Generate canonical name for local dependencies using source context
659            let canonical_name =
660                if let Some(manifest_dir) = self.core.manifest().manifest_dir.as_ref() {
661                    // Get the full path to the local dependency
662                    let full_path = if Path::new(dep.get_path()).is_absolute() {
663                        PathBuf::from(dep.get_path())
664                    } else {
665                        manifest_dir.join(dep.get_path())
666                    };
667
668                    // Normalize the path to handle ../ and ./ components deterministically
669                    // Use normalize_path instead of canonicalize() to avoid filesystem-dependent behavior
670                    // that can cause non-deterministic results across runs
671                    let canonical_path = crate::utils::fs::normalize_path(&full_path);
672
673                    let source_context =
674                        crate::resolver::source_context::SourceContext::local(manifest_dir);
675                    generate_dependency_name(&canonical_path.to_string_lossy(), &source_context)
676                } else {
677                    // Fallback to name if manifest_dir is not available
678                    name.to_string()
679                };
680
681            let variant_inputs = lockfile_builder::VariantInputs::new(
682                lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
683            );
684
685            Ok(LockedResource {
686                name: canonical_name,
687                source: None,
688                url: None,
689                path: normalize_path_for_storage(dep.get_path()),
690                version: None,
691                resolved_commit: None,
692                checksum: String::new(),
693                installed_at,
694                dependencies: self.get_dependencies_for(
695                    name,
696                    None,
697                    resource_type,
698                    Some(&artifact_type_string),
699                    variant_inputs.hash(),
700                ),
701                resource_type,
702                tool: Some(artifact_type_string),
703                manifest_alias,
704                applied_patches,
705                install: dep.get_install(),
706                variant_inputs,
707                context_checksum: None,
708            })
709        } else {
710            // Remote dependency - use canonical naming for consistency
711            let source_name = dep
712                .get_source()
713                .ok_or_else(|| anyhow::anyhow!("Dependency '{}' has no source specified", name))?;
714
715            // Generate canonical name using remote source context
716            let source_context =
717                crate::resolver::source_context::SourceContext::remote(source_name);
718            let canonical_name = generate_dependency_name(dep.get_path(), &source_context);
719
720            let source_url = self
721                .core
722                .source_manager()
723                .get_source_url(source_name)
724                .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
725
726            let version_key =
727                dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
728            let group_key = format!("{}::{}", source_name, version_key);
729
730            let prepared =
731                self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
732                    anyhow::anyhow!(
733                        "Prepared state missing for source '{}' @ '{}'",
734                        source_name,
735                        version_key
736                    )
737                })?;
738
739            let filename = if let Some(custom_filename) = dep.get_filename() {
740                custom_filename.to_string()
741            } else {
742                Path::new(dep.get_path()).to_string_lossy().to_string()
743            };
744
745            let artifact_type_string = dep
746                .get_tool()
747                .map(|s| s.to_string())
748                .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
749            let artifact_type = artifact_type_string.as_str();
750
751            let installed_at = install_path_resolver::resolve_install_path(
752                self.core.manifest(),
753                dep,
754                artifact_type,
755                resource_type,
756                &filename,
757            )?;
758
759            // Determine manifest_alias: only set for direct manifest dependencies or pattern-expanded
760            let manifest_alias = if let Some(pattern_alias) =
761                self.get_pattern_alias_for_dependency(name, resource_type)
762            {
763                // Pattern-expanded dependency - use pattern name as manifest_alias
764                Some(pattern_alias)
765            } else if self
766                .core
767                .manifest()
768                .get_dependencies(resource_type)
769                .is_some_and(|deps| deps.contains_key(name))
770            {
771                // Direct manifest dependency - use name as manifest_alias
772                Some(name.to_string())
773            } else {
774                // Transitive dependency - no manifest_alias
775                None
776            };
777
778            let applied_patches = lockfile_builder::get_patches_for_resource(
779                self.core.manifest(),
780                resource_type,
781                name,
782                manifest_alias.as_deref(),
783            );
784
785            let variant_inputs = lockfile_builder::VariantInputs::new(
786                lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
787            );
788
789            Ok(LockedResource {
790                name: canonical_name, // Use canonical name for internal consistency
791                source: Some(source_name.to_string()),
792                url: Some(source_url.clone()),
793                path: normalize_path_for_storage(dep.get_path()),
794                version: prepared.resolved_version.clone(),
795                resolved_commit: Some(prepared.resolved_commit.clone()),
796                checksum: String::new(),
797                installed_at,
798                dependencies: self.get_dependencies_for(
799                    name,
800                    Some(source_name),
801                    resource_type,
802                    Some(&artifact_type_string),
803                    variant_inputs.hash(),
804                ),
805                resource_type,
806                tool: Some(artifact_type_string),
807                manifest_alias,
808                applied_patches,
809                install: dep.get_install(),
810                variant_inputs,
811                context_checksum: None,
812            })
813        }
814    }
815
816    /// Resolve a pattern dependency to multiple locked resources.
817    async fn resolve_pattern_dependency(
818        &mut self,
819        name: &str,
820        dep: &ResourceDependency,
821        resource_type: ResourceType,
822    ) -> Result<Vec<LockedResource>> {
823        use crate::pattern::PatternResolver;
824        use crate::resolver::{
825            lockfile_builder, path_resolver as install_path_resolver, path_resolver,
826        };
827        use crate::utils::{
828            compute_relative_install_path, normalize_path, normalize_path_for_storage,
829        };
830
831        if !dep.is_pattern() {
832            return Err(anyhow::anyhow!(
833                "Expected pattern dependency but no glob characters found in path"
834            ));
835        }
836
837        let pattern = dep.get_path();
838
839        if dep.is_local() {
840            // Local pattern
841            let (base_path, pattern_str) = path_resolver::parse_pattern_base_path(pattern);
842            let pattern_resolver = PatternResolver::new();
843            let matches = pattern_resolver.resolve(&pattern_str, &base_path)?;
844
845            let artifact_type_string = dep
846                .get_tool()
847                .map(|s| s.to_string())
848                .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
849            let artifact_type = artifact_type_string.as_str();
850
851            // Compute variant inputs once for all matched files in the pattern
852            let variant_inputs = lockfile_builder::VariantInputs::new(
853                lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
854            );
855
856            let mut resources = Vec::new();
857            for matched_path in matches {
858                let resource_name = crate::pattern::extract_resource_name(&matched_path);
859                let full_relative_path =
860                    path_resolver::construct_full_relative_path(&base_path, &matched_path);
861                let filename = path_resolver::extract_pattern_filename(&base_path, &matched_path);
862
863                let installed_at = install_path_resolver::resolve_install_path(
864                    self.core.manifest(),
865                    dep,
866                    artifact_type,
867                    resource_type,
868                    &filename,
869                )?;
870
871                resources.push(LockedResource {
872                    name: resource_name.clone(),
873                    source: None,
874                    url: None,
875                    path: full_relative_path,
876                    version: None,
877                    resolved_commit: None,
878                    checksum: String::new(),
879                    installed_at,
880                    dependencies: vec![],
881                    resource_type,
882                    tool: Some(artifact_type_string.clone()),
883                    manifest_alias: Some(name.to_string()),
884                    applied_patches: lockfile_builder::get_patches_for_resource(
885                        self.core.manifest(),
886                        resource_type,
887                        &resource_name, // Use canonical resource name
888                        Some(name),     // Use manifest_alias for patch lookups
889                    ),
890                    install: dep.get_install(),
891                    variant_inputs: variant_inputs.clone(),
892                    context_checksum: None,
893                });
894            }
895
896            Ok(resources)
897        } else {
898            // Remote pattern
899            // Preserve the original pattern name since it might be shadowed later
900            let pattern_name = name;
901
902            let source_name = dep.get_source().ok_or_else(|| {
903                anyhow::anyhow!("Pattern dependency '{}' has no source specified", name)
904            })?;
905
906            let source_url = self
907                .core
908                .source_manager()
909                .get_source_url(source_name)
910                .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
911
912            let version_key =
913                dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
914            let group_key = format!("{}::{}", source_name, version_key);
915
916            let prepared =
917                self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
918                    anyhow::anyhow!(
919                        "Prepared state missing for source '{}' @ '{}'",
920                        source_name,
921                        version_key
922                    )
923                })?;
924
925            let repo_path = Path::new(&prepared.worktree_path);
926            let pattern_resolver = PatternResolver::new();
927            let matches = pattern_resolver.resolve(pattern, repo_path)?;
928
929            let artifact_type_string = dep
930                .get_tool()
931                .map(|s| s.to_string())
932                .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
933            let artifact_type = artifact_type_string.as_str();
934
935            // Compute variant inputs once for all matched files in the pattern
936            let variant_inputs = lockfile_builder::VariantInputs::new(
937                lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
938            );
939
940            let mut resources = Vec::new();
941            for matched_path in matches {
942                let resource_name = crate::pattern::extract_resource_name(&matched_path);
943
944                // Compute installation path
945                let installed_at = match resource_type {
946                    ResourceType::Hook | ResourceType::McpServer => {
947                        install_path_resolver::resolve_merge_target_path(
948                            self.core.manifest(),
949                            artifact_type,
950                            resource_type,
951                        )
952                    }
953                    _ => {
954                        let artifact_path = self
955                            .core
956                            .manifest()
957                            .get_artifact_resource_path(artifact_type, resource_type)
958                            .ok_or_else(|| {
959                                anyhow::anyhow!(
960                                    "Resource type '{}' is not supported by tool '{}'",
961                                    resource_type,
962                                    artifact_type
963                                )
964                            })?;
965
966                        let dep_flatten = dep.get_flatten();
967                        let tool_flatten = self
968                            .core
969                            .manifest()
970                            .get_tool_config(artifact_type)
971                            .and_then(|config| config.resources.get(resource_type.to_plural()))
972                            .and_then(|resource_config| resource_config.flatten);
973
974                        let flatten = dep_flatten.or(tool_flatten).unwrap_or(false);
975
976                        let base_target = if let Some(custom_target) = dep.get_target() {
977                            PathBuf::from(artifact_path.display().to_string())
978                                .join(custom_target.trim_start_matches('/'))
979                        } else {
980                            artifact_path.to_path_buf()
981                        };
982
983                        let filename = repo_path.join(&matched_path).to_string_lossy().to_string();
984                        let relative_path = compute_relative_install_path(
985                            &base_target,
986                            Path::new(&filename),
987                            flatten,
988                        );
989                        normalize_path_for_storage(normalize_path(&base_target.join(relative_path)))
990                    }
991                };
992
993                resources.push(LockedResource {
994                    name: resource_name.clone(),
995                    source: Some(source_name.to_string()),
996                    url: Some(source_url.clone()),
997                    path: normalize_path_for_storage(matched_path.to_string_lossy().to_string()),
998                    version: prepared.resolved_version.clone(),
999                    resolved_commit: Some(prepared.resolved_commit.clone()),
1000                    checksum: String::new(),
1001                    installed_at,
1002                    dependencies: vec![],
1003                    resource_type,
1004                    tool: Some(artifact_type_string.clone()),
1005                    manifest_alias: Some(pattern_name.to_string()),
1006                    applied_patches: lockfile_builder::get_patches_for_resource(
1007                        self.core.manifest(),
1008                        resource_type,
1009                        &resource_name,     // Use canonical resource name
1010                        Some(pattern_name), // Use manifest_alias for patch lookups
1011                    ),
1012                    install: dep.get_install(),
1013                    variant_inputs: variant_inputs.clone(),
1014                    context_checksum: None,
1015                });
1016            }
1017
1018            Ok(resources)
1019        }
1020    }
1021
1022    /// Add or update a lockfile entry with deduplication.
1023    fn add_or_update_lockfile_entry(
1024        &self,
1025        lockfile: &mut LockFile,
1026        _name: &str,
1027        entry: LockedResource,
1028    ) {
1029        let resources = lockfile.get_resources_mut(&entry.resource_type);
1030
1031        if let Some(existing) =
1032            resources.iter_mut().find(|e| lockfile_builder::is_duplicate_entry(e, &entry))
1033        {
1034            // Replace only if the new entry is more authoritative than the existing one
1035            // Priority: Direct (manifest_alias != None) > Transitive (manifest_alias == None)
1036            let existing_is_direct = existing.manifest_alias.is_some();
1037            let new_is_direct = entry.manifest_alias.is_some();
1038
1039            if new_is_direct || !existing_is_direct {
1040                // Replace if:
1041                // - New is direct (always wins)
1042                // - Both are transitive (newer wins)
1043                tracing::debug!(
1044                    "Replacing {} (direct={}) with {} (direct={})",
1045                    existing.name,
1046                    existing_is_direct,
1047                    entry.name,
1048                    new_is_direct
1049                );
1050                *existing = entry;
1051            } else {
1052                // Keep existing direct entry, ignore transitive replacement
1053                tracing::debug!("Keeping direct {} over transitive {}", existing.name, entry.name);
1054            }
1055        } else {
1056            resources.push(entry);
1057        }
1058    }
1059
1060    /// Add version information to dependency references in lockfile.
1061    fn add_version_to_dependencies(&self, lockfile: &mut LockFile) -> Result<()> {
1062        use crate::resolver::lockfile_builder;
1063
1064        lockfile_builder::add_version_to_all_dependencies(lockfile);
1065        Ok(())
1066    }
1067
1068    /// Detect target path conflicts between resources.
1069    fn detect_target_conflicts(&self, lockfile: &LockFile) -> Result<()> {
1070        use crate::resolver::lockfile_builder;
1071
1072        lockfile_builder::detect_target_conflicts(lockfile)
1073    }
1074
1075    /// Add a dependency to the conflict detector.
1076    fn add_to_conflict_detector(
1077        &mut self,
1078        _name: &str,
1079        dep: &ResourceDependency,
1080        required_by: &str,
1081    ) {
1082        use crate::resolver::types as dependency_helpers;
1083
1084        // Skip local dependencies (no version conflicts possible)
1085        if dep.is_local() {
1086            return;
1087        }
1088
1089        // Build resource identifier
1090        let resource_id = dependency_helpers::build_resource_id(dep);
1091
1092        // Get version constraint (None means HEAD/unspecified)
1093        let version = dep.get_version().unwrap_or("HEAD");
1094
1095        // Add to conflict detector
1096        self.conflict_detector.add_requirement(&resource_id, required_by, version);
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use super::*;
1103
1104    #[tokio::test]
1105    async fn test_resolver_creation() {
1106        let manifest = Manifest::default();
1107        let cache = Cache::new().unwrap();
1108        let resolver = DependencyResolver::new(manifest, cache).await;
1109        assert!(resolver.is_ok());
1110    }
1111
1112    #[tokio::test]
1113    async fn test_resolver_with_global() {
1114        let manifest = Manifest::default();
1115        let cache = Cache::new().unwrap();
1116        let resolver = DependencyResolver::new_with_global(manifest, cache).await;
1117        assert!(resolver.is_ok());
1118    }
1119}