Expand description
Dependency resolution and conflict detection for AGPM.
This module implements the core dependency resolution algorithm that transforms manifest dependencies into locked versions. It handles version constraint solving, conflict detection, transitive dependency resolution, parallel source synchronization, and relative path preservation during installation.
§Architecture Overview
The resolver operates using a two-phase architecture optimized for SHA-based worktree caching:
§Phase 1: Source Synchronization (pre_sync_sources
)
- Purpose: Perform all Git network operations upfront during “Syncing sources” phase
- Operations: Clone/fetch repositories, update refs, populate cache
- Benefits: Clear progress reporting, batch network operations, error isolation
- Result: All required repositories cached locally for phase 2
§Phase 2: Version Resolution (resolve
or update
)
- Purpose: Resolve versions to commit SHAs using cached repositories
- Operations: Parse dependencies, resolve constraints, detect conflicts, create worktrees, preserve paths
- Benefits: Fast local operations, no network I/O, deterministic behavior
- Result: Locked dependencies ready for installation with preserved directory structure
This two-phase approach replaces the previous three-phase model and provides better separation of concerns between network operations and dependency resolution logic.
§Algorithm Complexity
- Time: O(n + s·log(t)) where:
- n = number of dependencies
- s = number of unique sources
- t = average number of tags/branches per source
- Space: O(n + s) for dependency graph and source cache
§Parallel Processing
The resolver leverages async/await for concurrent operations:
- Sources are synchronized in parallel using
tokio::spawn
- Git operations are batched to minimize network roundtrips
- Progress reporting provides real-time feedback on long-running operations
§Resolution Process
The two-phase dependency resolution follows these steps:
§Phase 1: Source Synchronization
- Dependency Collection: Extract all dependencies from manifest
- Source Validation: Verify all referenced sources exist and are accessible
- Repository Preparation: Use
version_resolver::VersionResolver
to collect unique sources - Source Synchronization: Clone/update source repositories with single fetch per repository
- Cache Population: Store bare repository paths for phase 2 operations
§Phase 2: Version Resolution & Installation
- Batch SHA Resolution: Resolve all collected versions to commit SHAs using cached repositories
- SHA-based Worktree Creation: Create worktrees keyed by commit SHA for maximum deduplication
- Conflict Detection: Check for path conflicts and incompatible versions
- Redundancy Analysis: Identify duplicate resources across sources
- Path Processing: Preserve directory structure by using paths directly from dependencies
- Resource Installation: Copy resources to target locations with checksums
- Lockfile Generation: Create deterministic lockfile entries with resolved SHAs and preserved paths
This separation ensures all network operations complete in phase 1, while phase 2 operates entirely on cached data for fast, deterministic resolution.
§Version Resolution Strategy
Version constraints are resolved using the following precedence:
- Exact commits: SHA hashes are used directly
- Tags: Semantic version tags (e.g.,
v1.2.3
) are preferred - Branches: Branch heads are resolved to current commits
- Latest: Defaults to the default branch (usually
main
ormaster
)
§Conflict Detection
The resolver detects several types of conflicts:
§Version Conflicts
# Incompatible version constraints for the same resource
[agents]
app = { source = "community", path = "agents/helper.md", version = "v1.0.0" }
tool = { source = "community", path = "agents/helper.md", version = "v2.0.0" }
§Path Conflicts
# Different resources installing to the same location
[agents]
helper-v1 = { source = "old", path = "agents/helper.md" }
helper-v2 = { source = "new", path = "agents/helper.md" }
§Source Conflicts
When the same resource path exists in multiple sources with different content, the resolver uses source precedence (global config sources override local manifest sources).
§Security Considerations
The resolver implements several security measures:
- Input Validation: All Git references are validated before checkout
- Path Sanitization: Installation paths are validated to prevent directory traversal
- Credential Isolation: Authentication tokens are never stored in manifest files
- Checksum Verification: Resources are checksummed for integrity validation
§Performance Optimizations
- SHA-based Worktree Caching: Worktrees keyed by commit SHA maximize reuse across versions
- Batch Version Resolution: All versions resolved to SHAs upfront via
version_resolver::VersionResolver
- Single Fetch Per Repository: Command-instance fetch caching eliminates redundant network operations
- Source Caching: Git repositories are cached globally in
~/.agpm/cache/
- Incremental Updates: Only modified sources are re-synchronized
- Parallel Operations: Source syncing and version resolution run concurrently
- Progress Batching: UI updates are throttled to prevent performance impact
§Error Handling
The resolver provides detailed error context for common failure scenarios:
- Network Issues: Graceful handling of Git clone/fetch failures
- Authentication: Clear error messages for credential problems
- Version Mismatches: Specific guidance for constraint resolution failures
- Path Issues: Detailed information about file system conflicts
§Example Usage
§Two-Phase Resolution Pattern
use agpm_cli::resolver::DependencyResolver;
use agpm_cli::manifest::Manifest;
use agpm_cli::cache::Cache;
use std::path::Path;
let manifest = Manifest::load(Path::new("agpm.toml"))?;
let cache = Cache::new()?;
let mut resolver = DependencyResolver::new_with_global(manifest.clone(), cache).await?;
// Get all dependencies from manifest
let deps: Vec<(String, agpm_cli::manifest::ResourceDependency)> = manifest
.all_dependencies()
.into_iter()
.map(|(name, dep)| (name.to_string(), dep.clone()))
.collect();
// Phase 1: Sync all required sources (network operations)
resolver.pre_sync_sources(&deps).await?;
// Phase 2: Resolve dependencies using cached repositories (local operations)
let lockfile = resolver.resolve().await?;
println!("Resolved {} agents and {} snippets",
lockfile.agents.len(), lockfile.snippets.len());
§Update Pattern
let manifest = Manifest::load(Path::new("agpm.toml"))?;
let mut lockfile = LockFile::load(Path::new("agpm.lock"))?;
let cache = Cache::new()?;
let mut resolver = DependencyResolver::with_cache(manifest.clone(), cache);
// Get dependencies to update
let deps: Vec<(String, agpm_cli::manifest::ResourceDependency)> = manifest
.all_dependencies()
.into_iter()
.map(|(name, dep)| (name.to_string(), dep.clone()))
.collect();
// Phase 1: Sync sources for update
resolver.pre_sync_sources(&deps).await?;
// Phase 2: Update specific dependencies
resolver.update(&mut lockfile, None).await?;
lockfile.save(Path::new("agpm.lock"))?;
§Incremental Updates
use agpm_cli::resolver::DependencyResolver;
use agpm_cli::lockfile::LockFile;
use agpm_cli::cache::Cache;
use std::path::Path;
let existing = LockFile::load("agpm.lock".as_ref())?;
let manifest = agpm_cli::manifest::Manifest::load("agpm.toml".as_ref())?;
let cache = Cache::new()?;
let mut resolver = DependencyResolver::new(manifest, cache)?;
// Update specific dependencies only
let deps_to_update = vec!["agent1".to_string(), "snippet2".to_string()];
let deps_count = deps_to_update.len();
let updated = resolver.update(&existing, Some(deps_to_update)).await?;
println!("Updated {} dependencies", deps_count);
Modules§
- dependency_
graph - Dependency graph management for transitive dependency resolution.
- version_
resolution - Version constraint resolution helpers for the dependency resolver.
- version_
resolver - Centralized version resolution module for AGPM
Structs§
- Dependency
Resolver - Core dependency resolver that transforms manifest dependencies into lockfile entries.
Functions§
- extract_
meaningful_ path - Extract meaningful path structure from a dependency path.