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