agpm_cli/version/
mod.rs

1//! Version constraint parsing, comparison, and resolution for AGPM dependencies.
2//!
3//! This module provides comprehensive version management for AGPM, handling semantic
4//! versioning, Git references, and dependency resolution. It supports multiple version
5//! specification formats and provides sophisticated constraint resolution with conflict
6//! detection and prerelease handling.
7//!
8//! # Module Organization
9//!
10//! - [`constraints`] - Version constraint parsing, sets, and resolution
11//! - [`comparison`] - Version comparison utilities and analysis
12//! - Core types and functions for Git tag resolution
13//!
14//! # Version Specifications
15//!
16//! AGPM supports several version specification formats:
17//!
18//! ## Semantic Versions
19//! - **Exact versions**: `"1.0.0"` - Matches exactly the specified version
20//! - **Caret ranges**: `"^1.0.0"` - Compatible within major version (1.x.x)
21//! - **Tilde ranges**: `"~1.2.0"` - Compatible within minor version (1.2.x)
22//! - **Comparison ranges**: `">=1.0.0"`, `"<2.0.0"`, `">=1.0.0, <2.0.0"`
23//!
24//! ## Special Keywords
25//! - **Wildcard**: `"*"` - Matches any version
26//!
27//! ## Git References
28//! - **Branches**: `"main"`, `"develop"`, `"feature/auth"`
29//! - **Tags**: `"v1.0.0"`, `"release-2023-01"`
30//! - **Commits**: `"abc123..."` (full or abbreviated SHA)
31//!
32//! # Version Resolution Strategy
33//!
34//! The version resolution system follows this process:
35//!
36//! 1. **Tag Discovery**: Fetch all tags from the Git repository
37//! 2. **Semantic Parsing**: Parse tags as semantic versions where possible
38//! 3. **Constraint Matching**: Apply version constraints to find candidates
39//! 4. **Best Selection**: Choose the highest compatible version
40//! 5. **Fallback Handling**: Use branches or commits if no tags match
41//!
42//! # Constraint Resolution Features
43//!
44//! - **Multi-constraint support**: Combine multiple constraints per dependency
45//! - **Conflict detection**: Prevent impossible constraint combinations
46//! - **Prerelease handling**: Sophisticated alpha/beta/RC version management
47//! - **Cross-dependency resolution**: Resolve entire dependency graphs
48//!
49//! # Examples
50//!
51//! ## Basic Git Tag Resolution
52//!
53//! ```rust,no_run
54//! use agpm_cli::version::{VersionResolver, VersionInfo};
55//! use agpm_cli::git::GitRepo;
56//! use std::path::PathBuf;
57//!
58//! # async fn example() -> anyhow::Result<()> {
59//! let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
60//! let resolver = VersionResolver::from_git_tags(&repo).await?;
61//!
62//! // Resolve different constraint types
63//! if let Ok(Some(version)) = resolver.resolve("^1.0.0") {
64//!     println!("Resolved caret constraint to: {}", version.tag);
65//! }
66//! # Ok(())
67//! # }
68//! ```
69//!
70//! ## Advanced Constraint Resolution
71//!
72//! ```rust,no_run
73//! use agpm_cli::version::constraints::{ConstraintResolver, VersionConstraint};
74//! use semver::Version;
75//! use std::collections::HashMap;
76//!
77//! # fn example() -> anyhow::Result<()> {
78//! let mut resolver = ConstraintResolver::new();
79//!
80//! // Add constraints for multiple dependencies
81//! resolver.add_constraint("web-framework", "^2.0.0")?;
82//! resolver.add_constraint("database", "~1.5.0")?;
83//! resolver.add_constraint("auth-lib", ">=3.0.0")?;
84//!
85//! // Provide available versions
86//! let mut available = HashMap::new();
87//! available.insert("web-framework".to_string(), vec![Version::parse("2.1.0")?]);
88//! available.insert("database".to_string(), vec![Version::parse("1.5.3")?]);
89//! available.insert("auth-lib".to_string(), vec![Version::parse("3.2.0")?]);
90//!
91//! // Resolve all dependencies simultaneously
92//! let resolved = resolver.resolve(&available)?;
93//! println!("Resolved {} dependencies", resolved.len());
94//! # Ok(())
95//! # }
96//! ```
97//!
98//! ## Version Comparison and Analysis
99//!
100//! ```rust,no_run
101//! use agpm_cli::version::comparison::VersionComparator;
102//!
103//! # fn example() -> anyhow::Result<()> {
104//! let available_versions = vec![
105//!     "v1.0.0".to_string(),
106//!     "v1.5.0".to_string(),
107//!     "v2.0.0".to_string(),
108//! ];
109//!
110//! // Check for newer versions
111//! let has_updates = VersionComparator::has_newer_version("1.2.0", &available_versions)?;
112//! println!("Updates available: {}", has_updates);
113//!
114//! // Get all newer versions sorted by recency
115//! let newer = VersionComparator::get_newer_versions("1.2.0", &available_versions)?;
116//! for version in newer {
117//!     println!("Newer version: {}", version);
118//! }
119//!
120//! // Find the latest version
121//! if let Some(latest) = VersionComparator::get_latest(&available_versions)? {
122//!     println!("Latest version: {}", latest);
123//! }
124//! # Ok(())
125//! # }
126//! ```
127//!
128//! # Prerelease Version Handling
129//!
130//! AGPM provides sophisticated prerelease version management:
131//!
132//! - **Default exclusion**: Most constraints exclude prereleases for stability
133//! - **Explicit inclusion**: Use Git refs to include them
134//! - **Constraint inheritance**: If any constraint allows prereleases, all do
135//! - **Version precedence**: Stable versions are preferred when available
136//!
137//! # Error Handling
138//!
139//! The version system provides comprehensive error handling:
140//!
141//! - **Invalid version strings**: Malformed semantic versions are rejected
142//! - **Conflicting constraints**: Impossible combinations are detected early
143//! - **Missing dependencies**: Required dependencies without versions are flagged
144//! - **Resolution failures**: Unsatisfiable constraints are clearly reported
145//!
146//! # Cross-References
147//!
148//! - For detailed constraint syntax and resolution: [`constraints`]
149//! - For version comparison utilities: [`comparison`]
150//! - For Git repository integration: [`crate::git`]
151//! - For dependency management: [`crate::resolver`]
152
153use crate::git::GitRepo;
154use anyhow::{Context, Result};
155use regex::Regex;
156use semver::{Version, VersionReq};
157use serde::{Deserialize, Serialize};
158use std::sync::Arc;
159
160/// Parse a version requirement string, normalizing 'v' prefixes.
161///
162/// This helper function provides centralized semver parsing that handles both
163/// prefixed (`v1.0.0`, `^v1.0.0`) and unprefixed (`1.0.0`, `^1.0.0`) version strings.
164///
165/// # Arguments
166///
167/// * `requirement` - Version requirement string (e.g., "^v1.0.0", "~2.1.0", ">=v1.0.0")
168///
169/// # Returns
170///
171/// A parsed `VersionReq` if the requirement is valid semver syntax.
172///
173/// # Examples
174///
175/// ```
176/// use agpm_cli::version::parse_version_req;
177///
178/// // All of these parse successfully:
179/// assert!(parse_version_req("1.0.0").is_ok());
180/// assert!(parse_version_req("v1.0.0").is_ok());
181/// assert!(parse_version_req("^1.0.0").is_ok());
182/// assert!(parse_version_req("^v1.0.0").is_ok());
183/// assert!(parse_version_req("~v2.1.0").is_ok());
184/// assert!(parse_version_req(">=v1.0.0").is_ok());
185/// ```
186pub fn parse_version_req(requirement: &str) -> Result<VersionReq, semver::Error> {
187    // Strip 'v' prefix from version requirements
188    // Handles patterns like: "v1.0.0", "^v1.0.0", "~v2.1.0", "=v1.0.0", ">=v1.0.0", etc.
189    // We match 'v' at the start OR after operators to avoid breaking prerelease tags
190    // like "1.0.0-dev.1" or branch names like "develop"
191
192    static RE: std::sync::LazyLock<Regex> =
193        std::sync::LazyLock::new(|| Regex::new(r"(^|[~^=><])v").unwrap());
194
195    let normalized = RE.replace_all(requirement, "$1");
196
197    VersionReq::parse(&normalized)
198}
199
200/// Splits a version string into an optional prefix and the version/constraint part.
201///
202/// This function extracts versioned prefixes from tag names and version constraints,
203/// enabling support for prefixed versioning schemes like `agents-v1.0.0` or `snippets-^v2.0.0`.
204/// The prefix can contain hyphens and is separated from the version by detecting where
205/// the version pattern begins.
206///
207/// # Algorithm
208///
209/// Scans left-to-right to find the first occurrence of:
210/// - Constraint operators: `^`, `~`, `=`, `<`, `>`, `!`, `*`
211/// - Version prefix: `v` followed immediately by a digit
212/// - Bare digit (start of version number)
213///
214/// Everything before this point (minus trailing `-`) becomes the prefix.
215///
216/// # Arguments
217///
218/// * `s` - The string to parse (tag name or version constraint)
219///
220/// # Returns
221///
222/// A tuple of `(Option<String>, &str)` where:
223/// - First element is the prefix (if any)
224/// - Second element is the version/constraint string
225///
226/// # Examples
227///
228/// ```
229/// use agpm_cli::version::split_prefix_and_version;
230///
231/// // Prefixed versions
232/// assert_eq!(
233///     split_prefix_and_version("agents-v1.0.0"),
234///     (Some("agents".to_string()), "v1.0.0")
235/// );
236/// assert_eq!(
237///     split_prefix_and_version("my-tool-^v2.0.0"),
238///     (Some("my-tool".to_string()), "^v2.0.0")
239/// );
240///
241/// // Unprefixed versions
242/// assert_eq!(split_prefix_and_version("v1.0.0"), (None, "v1.0.0"));
243/// assert_eq!(split_prefix_and_version("^1.0.0"), (None, "^1.0.0"));
244///
245/// // Edge cases
246/// assert_eq!(
247///     split_prefix_and_version("tool-v-v1.0.0"),
248///     (Some("tool-v".to_string()), "v1.0.0")
249/// );
250/// ```
251#[inline]
252pub fn split_prefix_and_version(s: &str) -> (Option<String>, &str) {
253    // Iterate through characters with their byte indices (O(n) single pass)
254    for (byte_idx, ch) in s.char_indices() {
255        // Check for constraint operators or wildcard
256        if "^~=<>!*".contains(ch) {
257            return split_at_index(s, byte_idx);
258        }
259
260        // Check for 'v' followed by digit (version prefix)
261        if ch == 'v' {
262            // Look ahead to check next character (O(1) operation)
263            if s[byte_idx..].chars().nth(1).is_some_and(|next| next.is_ascii_digit()) {
264                return split_at_index(s, byte_idx);
265            }
266        }
267
268        // Check for bare digit (start of version number)
269        // Only treat as version start if:
270        // 1. At position 0 (start of string), OR
271        // 2. Immediately after a hyphen delimiter
272        if ch.is_ascii_digit() {
273            // Check if at start or after hyphen (O(1) operation)
274            let is_version_start = byte_idx == 0 || s[..byte_idx].ends_with('-');
275
276            if is_version_start {
277                return split_at_index(s, byte_idx);
278            }
279        }
280    }
281
282    // No version pattern found, entire string is the version/constraint
283    (None, s)
284}
285
286/// Helper function to split string at an index, extracting prefix if present.
287#[inline]
288fn split_at_index(s: &str, i: usize) -> (Option<String>, &str) {
289    if i == 0 {
290        // Version starts at beginning, no prefix
291        (None, s)
292    } else {
293        // Extract prefix (remove trailing hyphen)
294        let prefix = s[..i].trim_end_matches('-');
295        // Treat empty prefix as None (handles cases like "-v1.0.0")
296        if prefix.is_empty() {
297            (None, &s[i..])
298        } else {
299            (Some(prefix.to_string()), &s[i..])
300        }
301    }
302}
303
304/// Version information extracted from a Git tag.
305///
306/// `VersionInfo` represents a successfully parsed semantic version from a Git tag,
307/// along with metadata about the original tag and prerelease status. This structure
308/// is used throughout the version resolution system to maintain the connection
309/// between semantic versions and their source Git references.
310///
311/// # Fields
312///
313/// - `prefix`: Optional prefix for monorepo-style versioning (e.g., `"agents"` in `agents-v1.0.0`)
314/// - `version`: The parsed semantic version
315/// - `tag`: The original Git tag string (may include prefixes like `v` or `agents-v`)
316/// - `prerelease`: Whether this version contains prerelease identifiers
317///
318/// # Examples
319///
320/// ```rust,no_run
321/// use agpm_cli::version::VersionInfo;
322/// use semver::Version;
323///
324/// // Standard version without prefix
325/// let info = VersionInfo {
326///     prefix: None,
327///     version: Version::parse("1.0.0-beta.1").unwrap(),
328///     tag: "v1.0.0-beta.1".to_string(),
329///     prerelease: true,
330/// };
331///
332/// assert_eq!(info.prefix, None);
333/// assert_eq!(info.version.major, 1);
334/// assert_eq!(info.tag, "v1.0.0-beta.1");
335/// assert!(info.prerelease);
336///
337/// // Prefixed version for monorepo
338/// let prefixed = VersionInfo {
339///     prefix: Some("agents".to_string()),
340///     version: Version::parse("2.0.0").unwrap(),
341///     tag: "agents-v2.0.0".to_string(),
342///     prerelease: false,
343/// };
344///
345/// assert_eq!(prefixed.prefix, Some("agents".to_string()));
346/// assert_eq!(prefixed.version.major, 2);
347/// ```
348#[derive(Debug, Clone)]
349pub struct VersionInfo {
350    /// Optional prefix for versioned namespaces (e.g., "agents", "snippets")
351    pub prefix: Option<String>,
352    /// The parsed semantic version
353    pub version: Version,
354    /// The original Git tag string
355    pub tag: String,
356    /// Whether this is a prerelease version (alpha, beta, rc, etc.)
357    pub prerelease: bool,
358}
359
360/// Resolves semantic versions from Git repository tags.
361///
362/// `VersionResolver` provides the core functionality for discovering, parsing, and
363/// resolving semantic versions from Git tags. It handles tag discovery, version
364/// parsing, constraint matching, and best-version selection.
365///
366/// # Tag Processing
367///
368/// The resolver automatically:
369/// - Fetches all tags from a Git repository
370/// - Normalizes tag names (removes `v` prefixes, handles common formats)
371/// - Parses valid semantic versions (skips invalid tags)
372/// - Sorts versions in descending order (newest first)
373/// - Categorizes versions as stable or prerelease
374///
375/// # Resolution Strategy
376///
377/// When resolving version constraints:
378/// 1. **Exact versions** are matched with or without `v` prefixes
379/// 2. **Semantic ranges** are applied using semver matching rules (e.g., `^1.0.0`, `~2.1.0`)
380/// 3. **Tag/branch names** are matched exactly as fallback (including "latest" - just a name)
381/// 4. **Prerelease filtering** is applied based on constraint type
382///
383/// # Examples
384///
385/// ## Creating from Git Repository
386///
387/// ```rust,no_run
388/// use agpm_cli::version::VersionResolver;
389/// use agpm_cli::git::GitRepo;
390/// use std::path::PathBuf;
391///
392/// # async fn example() -> anyhow::Result<()> {
393/// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
394/// let resolver = VersionResolver::from_git_tags(&repo).await?;
395///
396/// println!("Found {} versions", resolver.list_all().len());
397/// # Ok(())
398/// # }
399/// ```
400///
401/// ## Version Resolution
402///
403/// ```rust,no_run
404/// # use agpm_cli::version::VersionResolver;
405/// # use agpm_cli::git::GitRepo;
406/// # use std::path::PathBuf;
407/// #
408/// # async fn example() -> anyhow::Result<()> {
409/// # let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
410/// # let resolver = VersionResolver::from_git_tags(&repo).await?;
411///
412/// // Resolve various constraint types
413/// if let Some(version) = resolver.resolve("^1.0.0")? {
414///     println!("Caret range resolved to: {} ({})", version.tag, version.version);
415/// }
416///
417/// if let Some(version) = resolver.resolve("v1.2.3")? {
418///     println!("Exact match: {}", version.tag);
419/// }
420/// # Ok(())
421/// # }
422/// ```
423pub struct VersionResolver {
424    versions: Vec<Arc<VersionInfo>>,
425}
426
427impl VersionResolver {
428    /// Create a new empty resolver with no versions.
429    ///
430    /// This constructor creates an empty resolver that contains no version information.
431    /// It's primarily useful for testing or as a starting point before adding versions
432    /// manually. For normal usage, prefer [`from_git_tags`](Self::from_git_tags) which
433    /// populates the resolver from a Git repository.
434    ///
435    /// # Examples
436    ///
437    /// ```rust,no_run
438    /// use agpm_cli::version::VersionResolver;
439    ///
440    /// let resolver = VersionResolver::new();
441    /// assert_eq!(resolver.list_all().len(), 0);
442    /// assert!(resolver.get_latest().is_none());
443    /// ```
444    #[must_use]
445    pub const fn new() -> Self {
446        Self {
447            versions: Vec::new(),
448        }
449    }
450
451    /// Create a resolver by discovering and parsing tags from a Git repository.
452    ///
453    /// This method performs the complete tag discovery and parsing workflow:
454    /// 1. **Fetch tags**: Retrieve all Git tags from the repository
455    /// 2. **Parse versions**: Attempt to parse each tag as a semantic version
456    /// 3. **Filter valid**: Keep only tags that parse successfully
457    /// 4. **Sort versions**: Order by semantic version (newest first)
458    /// 5. **Detect prereleases**: Identify versions with prerelease components
459    ///
460    /// # Arguments
461    ///
462    /// * `repo` - The [`GitRepo`] instance to discover tags from
463    ///
464    /// # Returns
465    ///
466    /// Returns `Ok(VersionResolver)` with parsed versions, or `Err` if Git
467    /// operations fail. Individual tag parsing failures are silently ignored.
468    ///
469    /// # Tag Parsing Rules
470    ///
471    /// - Common prefixes (`v`, `V`) are automatically stripped
472    /// - Invalid semantic versions are skipped (not included in resolver)
473    /// - Valid versions are sorted in descending order
474    /// - Prerelease status is detected from version components
475    ///
476    /// # Examples
477    ///
478    /// ```rust,no_run
479    /// use agpm_cli::version::VersionResolver;
480    /// use agpm_cli::git::GitRepo;
481    /// use std::path::PathBuf;
482    ///
483    /// # async fn example() -> anyhow::Result<()> {
484    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
485    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
486    ///
487    /// println!("Discovered {} valid versions", resolver.list_all().len());
488    ///
489    /// if let Some(latest) = resolver.get_latest() {
490    ///     println!("Latest version: {} ({})", latest.tag, latest.version);
491    /// }
492    /// # Ok(())
493    /// # }
494    /// ```
495    ///
496    /// # Error Handling
497    ///
498    /// This method returns errors for Git operations (repository access, tag listing)
499    /// but handles individual tag parsing failures gracefully by skipping invalid tags.
500    pub async fn from_git_tags(repo: &GitRepo) -> Result<Self> {
501        let tags = repo.list_tags().await?;
502        let mut versions = Vec::new();
503
504        for tag in tags {
505            if let Ok((prefix, version)) = Self::parse_tag(&tag) {
506                versions.push(Arc::new(VersionInfo {
507                    prefix,
508                    version: version.clone(),
509                    tag: tag.clone(),
510                    prerelease: !version.pre.is_empty(),
511                }));
512            }
513        }
514
515        // Sort versions in descending order (newest first)
516        versions.sort_by(|a, b| b.version.cmp(&a.version));
517
518        Ok(Self {
519            versions,
520        })
521    }
522
523    /// Parse a Git tag string into an optional prefix and semantic version.
524    ///
525    /// This internal method handles the extraction of versioned prefixes and parsing of
526    /// Git tag strings into semantic versions. It supports both prefixed tags (e.g.,
527    /// `agents-v1.0.0`) and unprefixed tags (e.g., `v1.0.0`).
528    ///
529    /// # Parsing Process
530    ///
531    /// 1. **Prefix extraction**: Use `split_prefix_and_version()` to separate prefix from version
532    /// 2. **Version normalization**: Strip `v` or `V` prefixes from version string
533    /// 3. **Semantic parsing**: Parse the cleaned string as a semantic version
534    /// 4. **Error context**: Provide helpful error messages for parsing failures
535    ///
536    /// # Arguments
537    ///
538    /// * `tag` - The Git tag string to parse
539    ///
540    /// # Returns
541    ///
542    /// Returns `Ok((Option<String>, Version))` where:
543    /// - First element is the optional prefix
544    /// - Second element is the parsed semantic version
545    ///
546    /// Returns `Err` for invalid semantic versions.
547    ///
548    /// # Examples
549    ///
550    /// ```rust,no_run
551    /// use agpm_cli::version::VersionResolver;
552    /// use semver::Version;
553    ///
554    /// // These would all parse successfully (if the method were public)
555    /// // "v1.0.0" → (None, Version::new(1, 0, 0))
556    /// // "agents-v2.1.3" → (Some("agents"), Version::new(2, 1, 3))
557    /// // "my-tool-v1.5.0-beta.1" → (Some("my-tool"), Version with prerelease)
558    /// ```
559    ///
560    /// # Implementation Note
561    ///
562    /// This method is private and used internally by [`from_git_tags`](Self::from_git_tags)
563    /// during the tag discovery and parsing process.
564    fn parse_tag(tag: &str) -> Result<(Option<String>, Version)> {
565        // Extract prefix and version string
566        let (prefix, version_str) = split_prefix_and_version(tag);
567
568        // Remove common version prefixes from the version part
569        let cleaned = version_str.trim_start_matches('v').trim_start_matches('V');
570
571        // Parse semantic version
572        let version = Version::parse(cleaned)
573            .with_context(|| format!("Failed to parse version from tag: {tag}"))?;
574
575        Ok((prefix, version))
576    }
577
578    /// Resolve a version requirement string to a specific version from available tags.
579    ///
580    /// This method applies version constraint logic to find the best matching version
581    /// from the resolver's collection of parsed Git tags. It supports various constraint
582    /// formats and applies appropriate matching rules for each type.
583    ///
584    /// # Constraint Resolution Order
585    ///
586    /// 1. **Exact versions**: Direct semantic version matches (with/without `v` prefix)
587    /// 2. **Version requirements**: Semver ranges like `"^1.0.0"`, `"~1.2.0"`, `"*"`
588    /// 3. **Tag names**: Exact tag string matching as fallback
589    ///
590    /// # Arguments
591    ///
592    /// * `requirement` - The version constraint string to resolve
593    ///
594    /// # Returns
595    ///
596    /// Returns `Ok(Some(VersionInfo))` if a matching version is found, `Ok(None)`
597    /// if no version satisfies the requirement, or `Err` for invalid requirements.
598    ///
599    /// # Prerelease Handling
600    ///
601    /// - **Default behavior**: Prereleases are excluded from semver range matching
602    /// - **Explicit matches**: Direct version/tag matches include prereleases
603    ///
604    /// # Examples
605    ///
606    /// ```rust,no_run
607    /// use agpm_cli::version::VersionResolver;
608    /// use agpm_cli::git::GitRepo;
609    /// use std::path::PathBuf;
610    ///
611    /// # async fn example() -> anyhow::Result<()> {
612    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
613    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
614    ///
615    /// // Exact version matching
616    /// if let Some(version) = resolver.resolve("1.2.3")? {
617    ///     println!("Found exact version: {}", version.tag);
618    /// }
619    ///
620    /// // Semver ranges
621    /// if let Some(version) = resolver.resolve("^1.0.0")? {
622    ///     println!("Compatible version: {} ({})", version.tag, version.version);
623    /// }
624    ///
625    /// // Tag name matching
626    /// if let Some(version) = resolver.resolve("v1.0.0-beta.1")? {
627    ///     println!("Tag match: {}", version.tag);
628    /// }
629    /// # Ok(())
630    /// # }
631    /// ```
632    ///
633    /// # Resolution Precedence
634    ///
635    /// When multiple versions could match:
636    /// - **Highest version wins**: Newer semantic versions are preferred
637    /// - **Stable over prerelease**: Stable versions preferred unless prereleases explicitly allowed
638    /// - **First match for tags**: Tag name matching returns the first occurrence
639    pub fn resolve(&self, requirement: &str) -> Result<Option<Arc<VersionInfo>>> {
640        // Extract prefix and version part (e.g., "agents-^v1.0.0" → (Some("agents"), "^v1.0.0"))
641        let (prefix, version_str) = split_prefix_and_version(requirement);
642
643        // Filter versions by prefix first
644        let matching_prefix: Vec<&Arc<VersionInfo>> =
645            self.versions.iter().filter(|v| v.prefix.as_ref() == prefix.as_ref()).collect();
646
647        // Try exact version match (with or without 'v' prefix)
648        if let Ok(exact_version) = Version::parse(version_str.trim_start_matches('v')) {
649            return Ok(matching_prefix
650                .iter()
651                .find(|v| v.version == exact_version)
652                .map(|&v| Arc::clone(v)));
653        }
654
655        // Try as semantic version requirement using centralized parser
656        if let Ok(req) = parse_version_req(version_str) {
657            return Ok(matching_prefix
658                .iter()
659                .filter(|v| !v.prerelease) // Exclude prereleases by default
660                .find(|v| req.matches(&v.version))
661                .map(|&v| Arc::clone(v)));
662        }
663
664        // Try exact tag match (full tag including prefix)
665        for version in &self.versions {
666            if version.tag == requirement {
667                return Ok(Some(Arc::clone(version)));
668            }
669        }
670
671        Ok(None)
672    }
673
674    /// Get the latest version including prereleases.
675    ///
676    /// This method returns the absolute newest version from the resolver's collection,
677    /// including prerelease versions. Since versions are sorted in descending order,
678    /// this simply returns the first version in the list.
679    ///
680    /// # Returns
681    ///
682    /// Returns `Some(VersionInfo)` with the highest version, or `None` if no versions
683    /// are available in the resolver.
684    ///
685    /// # Prerelease Inclusion
686    ///
687    /// Unlike [`get_latest_stable`](Self::get_latest_stable), this method includes
688    /// prerelease versions in consideration. If the highest version happens to be
689    /// a prerelease (e.g., `2.0.0-beta.1` when `1.9.0` is the latest stable),
690    /// the prerelease version will be returned.
691    ///
692    /// # Examples
693    ///
694    /// ```rust,no_run
695    /// use agpm_cli::version::VersionResolver;
696    /// use agpm_cli::git::GitRepo;
697    /// use std::path::PathBuf;
698    ///
699    /// # async fn example() -> anyhow::Result<()> {
700    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
701    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
702    ///
703    /// if let Some(latest) = resolver.get_latest() {
704    ///     println!("Absolute latest: {} (prerelease: {})",
705    ///              latest.tag, latest.prerelease);
706    /// } else {
707    ///     println!("No versions found in repository");
708    /// }
709    /// # Ok(())
710    /// # }
711    /// ```
712    ///
713    /// # Use Cases
714    ///
715    /// This method is useful when:
716    /// - You want the cutting-edge version regardless of stability
717    /// - Implementing `latest-prerelease` constraint resolution
718    /// - Analyzing the most recent development activity
719    #[must_use]
720    pub fn get_latest(&self) -> Option<Arc<VersionInfo>> {
721        self.versions.first().map(Arc::clone)
722    }
723
724    /// Get the latest stable version excluding prereleases.
725    ///
726    /// This method finds the newest version that doesn't contain prerelease identifiers
727    /// (such as `-alpha`, `-beta`, `-rc`). It's the preferred method for production
728    /// environments where stability is prioritized over cutting-edge features.
729    ///
730    /// # Returns
731    ///
732    /// Returns `Some(VersionInfo)` with the highest stable version, or `None` if no
733    /// stable versions are available (only prereleases exist).
734    ///
735    /// # Stability Definition
736    ///
737    /// A version is considered stable if its prerelease component is empty. This means:
738    /// - `1.0.0` is stable
739    /// - `1.0.0-beta.1` is not stable (has prerelease suffix)
740    /// - `1.0.0+build.123` is stable (build metadata doesn't affect stability)
741    ///
742    /// # Examples
743    ///
744    /// ```rust,no_run
745    /// use agpm_cli::version::VersionResolver;
746    /// use agpm_cli::git::GitRepo;
747    /// use std::path::PathBuf;
748    ///
749    /// # async fn example() -> anyhow::Result<()> {
750    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
751    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
752    ///
753    /// match resolver.get_latest_stable() {
754    ///     Some(stable) => {
755    ///         println!("Latest stable version: {}", stable.tag);
756    ///         assert!(!stable.prerelease); // Always false for stable versions
757    ///     }
758    ///     None => println!("No stable versions found (only prereleases available)"),
759    /// }
760    /// # Ok(())
761    /// # }
762    /// ```
763    ///
764    /// # Comparison with `get_latest()`
765    ///
766    /// ```rust,no_run
767    /// # use agpm_cli::version::VersionResolver;
768    /// # use agpm_cli::git::GitRepo;
769    /// # use std::path::PathBuf;
770    /// #
771    /// # async fn example() -> anyhow::Result<()> {
772    /// # let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
773    /// # let resolver = VersionResolver::from_git_tags(&repo).await?;
774    ///
775    /// let latest = resolver.get_latest();
776    /// let stable = resolver.get_latest_stable();
777    ///
778    /// // Latest might be a prerelease version
779    /// // Stable will always be a non-prerelease version (or None)
780    ///
781    /// if let (Some(l), Some(s)) = (latest, stable) {
782    ///     if l.version > s.version {
783    ///         println!("Newest version {} is a prerelease", l.tag);
784    ///         println!("Latest stable version is {}", s.tag);
785    ///     }
786    /// }
787    /// # Ok(())
788    /// # }
789    /// ```
790    ///
791    /// # Use Cases
792    ///
793    /// This method is ideal for:
794    /// - Production dependency resolution
795    /// - Implementing `"latest"` constraint resolution
796    /// - Default version selection in package managers
797    /// - Stable release identification
798    #[must_use]
799    pub fn get_latest_stable(&self) -> Option<Arc<VersionInfo>> {
800        self.versions.iter().find(|v| !v.prerelease).map(Arc::clone)
801    }
802
803    /// List all versions discovered from Git tags.
804    ///
805    /// This method returns a complete list of all successfully parsed versions from
806    /// the Git repository, including both stable and prerelease versions. The list
807    /// is sorted in descending order by semantic version (newest first).
808    ///
809    /// # Returns
810    ///
811    /// Returns `Vec<VersionInfo>` containing all parsed versions. The vector may be
812    /// empty if no valid semantic versions were found in the repository tags.
813    ///
814    /// # Sorting Order
815    ///
816    /// Versions are sorted by semantic version precedence in descending order:
817    /// - Higher major versions first (e.g., `2.0.0` before `1.9.0`)
818    /// - Higher minor versions within same major (e.g., `1.5.0` before `1.2.0`)
819    /// - Higher patch versions within same minor (e.g., `1.2.3` before `1.2.1`)
820    /// - Release versions before prereleases (e.g., `1.0.0` before `1.0.0-beta.1`)
821    ///
822    /// # Examples
823    ///
824    /// ```rust,no_run
825    /// use agpm_cli::version::VersionResolver;
826    /// use agpm_cli::git::GitRepo;
827    /// use std::path::PathBuf;
828    ///
829    /// # async fn example() -> anyhow::Result<()> {
830    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
831    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
832    ///
833    /// let all_versions = resolver.list_all();
834    /// println!("Found {} versions:", all_versions.len());
835    ///
836    /// for (i, version) in all_versions.iter().enumerate() {
837    ///     let status = if version.prerelease { "prerelease" } else { "stable" };
838    ///     println!("  {}. {} ({}) - {}", i + 1, version.tag, version.version, status);
839    /// }
840    /// # Ok(())
841    /// # }
842    /// ```
843    ///
844    /// ## Filtering and Analysis
845    ///
846    /// ```rust,no_run
847    /// # use agpm_cli::version::VersionResolver;
848    /// # use agpm_cli::git::GitRepo;
849    /// # use std::path::PathBuf;
850    /// #
851    /// # async fn example() -> anyhow::Result<()> {
852    /// # let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
853    /// # let resolver = VersionResolver::from_git_tags(&repo).await?;
854    ///
855    /// let all_versions = resolver.list_all();
856    ///
857    /// // Count prereleases vs stable
858    /// let prerelease_count = all_versions.iter().filter(|v| v.prerelease).count();
859    /// let stable_count = all_versions.len() - prerelease_count;
860    ///
861    /// println!("Stable versions: {}, Prereleases: {}", stable_count, prerelease_count);
862    ///
863    /// // Find versions in a specific range
864    /// let v1_versions: Vec<_> = all_versions.iter()
865    ///     .filter(|v| v.version.major == 1)
866    ///     .collect();
867    /// println!("Found {} versions in v1.x.x series", v1_versions.len());
868    /// # Ok(())
869    /// # }
870    /// ```
871    ///
872    /// # Use Cases
873    ///
874    /// This method is useful for:
875    /// - Version analysis and reporting
876    /// - Building version selection interfaces
877    /// - Debugging version resolution issues
878    /// - Implementing custom constraint logic
879    #[must_use]
880    pub fn list_all(&self) -> Vec<Arc<VersionInfo>> {
881        self.versions.clone()
882    }
883
884    /// List only stable versions excluding prereleases.
885    ///
886    /// This method filters the complete version list to include only versions without
887    /// prerelease components. It's useful for scenarios where you need to work with
888    /// production-ready versions only.
889    ///
890    /// # Returns
891    ///
892    /// Returns `Vec<VersionInfo>` containing only stable versions, sorted in descending
893    /// order. The vector may be empty if no stable versions exist (only prereleases).
894    ///
895    /// # Filtering Criteria
896    ///
897    /// A version is included if:
898    /// - Its prerelease component is empty (no `-alpha`, `-beta`, `-rc` suffixes)
899    /// - It parses as a valid semantic version
900    /// - It was successfully extracted from a Git tag
901    ///
902    /// # Examples
903    ///
904    /// ```rust,no_run
905    /// use agpm_cli::version::VersionResolver;
906    /// use agpm_cli::git::GitRepo;
907    /// use std::path::PathBuf;
908    ///
909    /// # async fn example() -> anyhow::Result<()> {
910    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
911    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
912    ///
913    /// let stable_versions = resolver.list_stable();
914    /// println!("Found {} stable versions:", stable_versions.len());
915    ///
916    /// for version in stable_versions {
917    ///     println!("  {} ({})", version.tag, version.version);
918    ///     assert!(!version.prerelease); // Guaranteed to be false
919    /// }
920    /// # Ok(())
921    /// # }
922    /// ```
923    ///
924    /// ## Comparison with All Versions
925    ///
926    /// ```rust,no_run
927    /// # use agpm_cli::version::VersionResolver;
928    /// # use agpm_cli::git::GitRepo;
929    /// # use std::path::PathBuf;
930    /// #
931    /// # async fn example() -> anyhow::Result<()> {
932    /// # let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
933    /// # let resolver = VersionResolver::from_git_tags(&repo).await?;
934    ///
935    /// let all_versions = resolver.list_all();
936    /// let stable_versions = resolver.list_stable();
937    ///
938    /// println!("Total versions: {}", all_versions.len());
939    /// println!("Stable versions: {}", stable_versions.len());
940    /// println!("Prerelease versions: {}", all_versions.len() - stable_versions.len());
941    ///
942    /// if stable_versions.len() < all_versions.len() {
943    ///     println!("Repository contains prerelease versions");
944    /// }
945    /// # Ok(())
946    /// # }
947    /// ```
948    ///
949    /// # Use Cases
950    ///
951    /// This method is particularly useful for:
952    /// - Production environment version selection
953    /// - Conservative update strategies
954    /// - Compliance requirements that exclude prereleases
955    /// - User interfaces that hide development versions by default
956    #[must_use]
957    pub fn list_stable(&self) -> Vec<Arc<VersionInfo>> {
958        self.versions.iter().filter(|v| !v.prerelease).map(Arc::clone).collect()
959    }
960
961    /// Check if a specific version constraint can be resolved.
962    ///
963    /// This method tests whether a given version constraint string can be successfully
964    /// resolved against the available versions in this resolver. It's a convenience
965    /// method that combines resolution and existence checking.
966    ///
967    /// # Arguments
968    ///
969    /// * `version` - The version constraint string to test
970    ///
971    /// # Returns
972    ///
973    /// Returns `true` if the version constraint resolves to an actual version,
974    /// `false` if no matching version is found or if resolution fails.
975    ///
976    /// # Resolution Types Tested
977    ///
978    /// This method can verify existence of:
979    /// - **Exact versions**: `"1.0.0"`, `"v1.2.3"`
980    /// - **Version ranges**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"`
981    /// - **Tag/branch names**: Exact Git tag or branch matches (including "latest")
982    ///
983    /// # Examples
984    ///
985    /// ```rust,no_run
986    /// use agpm_cli::version::VersionResolver;
987    /// use agpm_cli::git::GitRepo;
988    /// use std::path::PathBuf;
989    ///
990    /// # async fn example() -> anyhow::Result<()> {
991    /// let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
992    /// let resolver = VersionResolver::from_git_tags(&repo).await?;
993    ///
994    /// // Check if specific versions exist
995    /// if resolver.has_version("1.0.0") {
996    ///     println!("Version 1.0.0 is available");
997    /// }
998    ///
999    /// if resolver.has_version("^1.0.0") {
1000    ///     println!("Compatible versions with 1.0.0 exist");
1001    /// }
1002    ///
1003    /// // This will likely return false unless you have this exact tag
1004    /// if resolver.has_version("v99.99.99") {
1005    ///     println!("Unlikely version found!");
1006    /// } else {
1007    ///     println!("Version 99.99.99 not found (as expected)");
1008    /// }
1009    /// # Ok(())
1010    /// # }
1011    /// ```
1012    ///
1013    /// ## Validation Before Resolution
1014    ///
1015    /// ```rust,no_run
1016    /// # use agpm_cli::version::VersionResolver;
1017    /// # use agpm_cli::git::GitRepo;
1018    /// # use std::path::PathBuf;
1019    /// #
1020    /// # async fn example() -> anyhow::Result<()> {
1021    /// # let repo = GitRepo::new(PathBuf::from("/path/to/repo"));
1022    /// # let resolver = VersionResolver::from_git_tags(&repo).await?;
1023    ///
1024    /// let constraint = "^2.0.0";
1025    ///
1026    /// if resolver.has_version(constraint) {
1027    ///     // Safe to resolve - we know it will succeed
1028    ///     let version = resolver.resolve(constraint)?.unwrap();
1029    ///     println!("Resolved {} to {}", constraint, version.tag);
1030    /// } else {
1031    ///     println!("No versions satisfy constraint: {}", constraint);
1032    /// }
1033    /// # Ok(())
1034    /// # }
1035    /// ```
1036    ///
1037    /// # Error Handling
1038    ///
1039    /// This method handles resolution errors gracefully by returning `false` rather
1040    /// than propagating errors. This makes it safe to use for validation without
1041    /// extensive error handling.
1042    ///
1043    /// # Use Cases
1044    ///
1045    /// This method is useful for:
1046    /// - Validating user input before processing
1047    /// - Pre-flight checks in dependency resolution
1048    /// - Conditional logic based on version availability
1049    /// - User interface validation and feedback
1050    #[must_use]
1051    pub fn has_version(&self, version: &str) -> bool {
1052        self.resolve(version).unwrap_or(None).is_some()
1053    }
1054}
1055
1056impl Default for VersionResolver {
1057    fn default() -> Self {
1058        Self::new()
1059    }
1060}
1061
1062/// Check if a version string satisfies a version requirement.
1063///
1064/// This utility function provides standalone version matching without requiring
1065/// a [`VersionResolver`] instance. It supports semantic version requirements and
1066/// special keywords for direct version-to-requirement comparison.
1067///
1068/// # Arguments
1069///
1070/// * `version` - The version string to test (supports `v` prefixes)
1071/// * `requirement` - The requirement string to match against
1072///
1073/// # Returns
1074///
1075/// Returns `Ok(true)` if the version satisfies the requirement, `Ok(false)` if it
1076/// doesn't match, or `Err` for invalid version/requirement strings.
1077///
1078/// # Supported Requirements
1079///
1080/// - **Special keywords**: `"*"` (wildcard, always matches)
1081/// - **Exact versions**: `"1.0.0"` (must match exactly)
1082/// - **Caret ranges**: `"^1.0.0"` (compatible within major version)
1083/// - **Tilde ranges**: `"~1.2.0"` (compatible within minor version)
1084/// - **Comparison ranges**: `">=1.0.0"`, `"<2.0.0"`
1085/// - **Complex ranges**: `">=1.0.0, <2.0.0"` (multiple constraints)
1086///
1087/// # Examples
1088///
1089/// ```rust,no_run
1090/// use agpm_cli::version::matches_requirement;
1091///
1092/// # fn example() -> anyhow::Result<()> {
1093/// // Exact version matching
1094/// assert!(matches_requirement("1.0.0", "1.0.0")?);
1095/// assert!(matches_requirement("v1.0.0", "1.0.0")?); // v prefix ignored
1096/// assert!(!matches_requirement("1.0.1", "1.0.0")?);
1097///
1098/// // Caret range matching (compatible within major version)
1099/// assert!(matches_requirement("1.2.3", "^1.0.0")?);
1100/// assert!(matches_requirement("1.9.9", "^1.0.0")?);
1101/// assert!(!matches_requirement("2.0.0", "^1.0.0")?); // Major version change
1102///
1103/// // Tilde range matching (compatible within minor version)
1104/// assert!(matches_requirement("1.2.5", "~1.2.0")?);
1105/// assert!(!matches_requirement("1.3.0", "~1.2.0")?); // Minor version change
1106///
1107/// // Comparison ranges
1108/// assert!(matches_requirement("1.5.0", ">=1.0.0")?);
1109/// assert!(!matches_requirement("0.9.0", ">=1.0.0")?);
1110///
1111/// // Wildcard
1112/// assert!(matches_requirement("any.version", "*")?);
1113/// # Ok(())
1114/// # }
1115/// ```
1116///
1117/// ## Complex Range Matching
1118///
1119/// ```rust,no_run
1120/// use agpm_cli::version::matches_requirement;
1121///
1122/// # fn example() -> anyhow::Result<()> {
1123/// // Multiple constraints
1124/// assert!(matches_requirement("1.5.0", ">=1.0.0, <2.0.0")?);
1125/// assert!(!matches_requirement("2.0.0", ">=1.0.0, <2.0.0")?);
1126///
1127/// // Pre-release handling
1128/// assert!(matches_requirement("1.0.0-beta.1", "^1.0.0-beta")?);
1129/// # Ok(())
1130/// # }
1131/// ```
1132///
1133/// # Version Prefix Handling
1134///
1135/// The function handles both namespace prefixes and `v` prefixes:
1136/// - `"v1.0.0"` is treated as `"1.0.0"`
1137/// - `"V2.1.3"` is treated as `"2.1.3"`
1138/// - `"agents-v1.2.0"` requires `"agents-^v1.0.0"` (prefixes must match)
1139/// - Unprefixed versions don't match prefixed requirements and vice versa
1140///
1141/// # Error Cases
1142///
1143/// This function returns errors for:
1144/// - Invalid semantic version strings
1145/// - Malformed requirement syntax
1146/// - Unparseable version ranges
1147///
1148/// # Use Cases
1149///
1150/// This function is useful for:
1151/// - Quick version compatibility checks
1152/// - Input validation in CLI tools
1153/// - Testing version constraints programmatically
1154/// - Implementing custom version resolution logic
1155pub fn matches_requirement(version: &str, requirement: &str) -> Result<bool> {
1156    // Extract prefixes from both version and requirement
1157    let (version_prefix, version_str) = split_prefix_and_version(version);
1158    let (req_prefix, req_str) = split_prefix_and_version(requirement);
1159
1160    // Ensure prefixes match (both None, or both Some with same value)
1161    if version_prefix != req_prefix {
1162        return Ok(false);
1163    }
1164
1165    // Handle wildcard in the version portion
1166    if req_str == "*" {
1167        return Ok(true);
1168    }
1169
1170    // Parse version (strip v prefix from version portion)
1171    let version = Version::parse(version_str.trim_start_matches('v'))?;
1172
1173    // Parse requirement (with v-prefix normalization)
1174    let req = parse_version_req(req_str)
1175        .map_err(|e| anyhow::anyhow!("Invalid version requirement '{requirement}': {e}"))?;
1176
1177    Ok(req.matches(&version))
1178}
1179
1180/// Parse a version constraint string into a structured constraint type.
1181///
1182/// This function analyzes a constraint string and determines whether it represents
1183/// a Git commit hash, a version/tag specification, or a branch name. It provides
1184/// a simple classification system for different types of version references.
1185///
1186/// # Classification Logic
1187///
1188/// The function uses heuristics to determine constraint types:
1189/// 1. **Commit hashes**: 7+ hexadecimal characters (e.g., `"abc123def"`)
1190/// 2. **Version/tag specs**: Valid semantic versions or requirements (e.g., `"^1.0.0"`, `"*"`)
1191/// 3. **Branch names**: Everything else (e.g., `"main"`, `"latest"`, `"feature/auth"`)
1192///
1193/// # Arguments
1194///
1195/// * `constraint` - The constraint string to parse and classify
1196///
1197/// # Returns
1198///
1199/// Returns a [`VersionConstraint`] enum variant indicating the constraint type:
1200/// - [`VersionConstraint::Commit`] for Git commit hashes
1201/// - [`VersionConstraint::Tag`] for semantic versions and requirements
1202/// - [`VersionConstraint::Branch`] for Git branch names
1203///
1204/// # Examples
1205///
1206/// ```rust,no_run
1207/// use agpm_cli::version::{parse_version_constraint, VersionConstraint};
1208///
1209/// // Semantic versions are classified as tags
1210/// let constraint = parse_version_constraint("1.0.0");
1211/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1212///
1213/// let constraint = parse_version_constraint("v1.2.3");
1214/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1215///
1216/// // Prefixed versions and constraints are also classified as tags
1217/// let constraint = parse_version_constraint("agents-v1.2.0");
1218/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1219///
1220/// let constraint = parse_version_constraint("agents-^v1.0.0");
1221/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1222///
1223/// // Version requirements are classified as tags
1224/// let constraint = parse_version_constraint("^1.0.0");
1225/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1226///
1227/// let constraint = parse_version_constraint("*");
1228/// assert!(matches!(constraint, VersionConstraint::Tag(_)));
1229///
1230/// // Commit hashes are detected by hex pattern
1231/// let constraint = parse_version_constraint("abc1234");
1232/// assert!(matches!(constraint, VersionConstraint::Commit(_)));
1233///
1234/// let constraint = parse_version_constraint("1234567890abcdef1234567890abcdef12345678");
1235/// assert!(matches!(constraint, VersionConstraint::Commit(_)));
1236///
1237/// // Branch names are the fallback
1238/// let constraint = parse_version_constraint("main");
1239/// assert!(matches!(constraint, VersionConstraint::Branch(_)));
1240///
1241/// let constraint = parse_version_constraint("feature/auth-system");
1242/// assert!(matches!(constraint, VersionConstraint::Branch(_)));
1243/// ```
1244///
1245/// # Commit Hash Detection
1246///
1247/// The function identifies commit hashes using these criteria:
1248/// - Minimum 7 characters (Git's default abbreviation length)
1249/// - All characters must be hexadecimal (0-9, a-f, A-F)
1250/// - No maximum length (supports full 40-character SHA-1 hashes)
1251///
1252/// # Version/Tag Detection
1253///
1254/// Version and tag specifications are identified by:
1255/// - Valid semantic version parsing (with or without `v` prefix)
1256/// - Valid semantic version requirement parsing (ranges, comparisons)
1257/// - Wildcard `"*"` for any version
1258///
1259/// # Branch Name Fallback
1260///
1261/// Any string that doesn't match the above patterns is treated as a branch name:
1262/// - Simple names: `"main"`, `"develop"`, `"staging"`, `"latest"`
1263/// - Namespaced branches: `"feature/new-ui"`, `"bugfix/auth-issue"`
1264/// - Special characters: `"release/v1.0"`, `"user/name/branch"`
1265///
1266/// # Use Cases
1267///
1268/// This function is useful for:
1269/// - Parsing user input in dependency specifications
1270/// - Routing version resolution to appropriate handlers
1271/// - Validating constraint syntax in configuration files
1272/// - Building version constraint objects from strings
1273#[must_use]
1274pub fn parse_version_constraint(constraint: &str) -> VersionConstraint {
1275    // Check if it looks like a commit hash (40 hex chars or abbreviated)
1276    if constraint.len() >= 7 && constraint.chars().all(|c| c.is_ascii_hexdigit()) {
1277        return VersionConstraint::Commit(constraint.to_string());
1278    }
1279
1280    // Extract prefix to check the version portion
1281    let (_prefix, version_str) = split_prefix_and_version(constraint);
1282
1283    // Check if the version portion is a semantic version or version requirement
1284    if Version::parse(version_str.trim_start_matches('v')).is_ok()
1285        || parse_version_req(version_str).is_ok()
1286        || version_str == "*"
1287    {
1288        return VersionConstraint::Tag(constraint.to_string());
1289    }
1290
1291    // Otherwise treat as branch
1292    VersionConstraint::Branch(constraint.to_string())
1293}
1294
1295/// Version comparison utilities and analysis functions.
1296///
1297/// The [`comparison`] module provides tools for analyzing and comparing semantic
1298/// versions, finding newer versions, and determining latest releases from version
1299/// collections. See the module documentation for detailed usage examples.
1300pub mod comparison;
1301
1302/// Version conflict detection and circular dependency detection.
1303///
1304/// The [`conflict`] module provides sophisticated conflict analysis for version
1305/// requirements, detecting incompatible version constraints and circular dependencies
1306/// in the dependency graph.
1307pub mod conflict;
1308
1309/// Version constraint parsing, sets, and resolution system.
1310///
1311/// The [`constraints`] module contains the core constraint management system for
1312/// AGPM, including constraint parsing, conflict detection, and multi-dependency
1313/// resolution. See the module documentation for comprehensive examples.
1314pub mod constraints;
1315
1316/// Represents different types of version constraints in AGPM.
1317///
1318/// `VersionConstraint` is a simple enum that categorizes version references into
1319/// three main types: Git tags (including semantic versions), Git branches, and
1320/// Git commit hashes. This classification helps AGPM route version resolution
1321/// to the appropriate handling logic.
1322///
1323/// # Variants
1324///
1325/// - [`Tag`](Self::Tag): Semantic versions, version requirements, and Git tags
1326/// - [`Branch`](Self::Branch): Git branch names and references
1327/// - [`Commit`](Self::Commit): Git commit hashes (full or abbreviated)
1328///
1329/// # Serialization
1330///
1331/// This enum implements [`Serialize`] and [`Deserialize`] for use in configuration
1332/// files and lockfiles, allowing version constraints to be persisted and restored.
1333///
1334/// # Examples
1335///
1336/// ```rust,no_run
1337/// use agpm_cli::version::VersionConstraint;
1338///
1339/// // Create different constraint types
1340/// let version = VersionConstraint::Tag("1.0.0".to_string());
1341/// let branch = VersionConstraint::Branch("main".to_string());
1342/// let commit = VersionConstraint::Commit("abc123def".to_string());
1343///
1344/// // Access the string value
1345/// assert_eq!(version.as_str(), "1.0.0");
1346/// assert_eq!(branch.as_str(), "main");
1347/// assert_eq!(commit.as_str(), "abc123def");
1348/// ```
1349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1350pub enum VersionConstraint {
1351    /// A semantic version tag (e.g., "v1.2.0", "1.0.0")
1352    Tag(String),
1353    /// A git branch reference (e.g., "main", "develop", "feature/new")
1354    Branch(String),
1355    /// A specific git commit hash (full or abbreviated)
1356    Commit(String),
1357}
1358
1359impl VersionConstraint {
1360    /// Get the string representation of this constraint.
1361    ///
1362    /// This method extracts the underlying string value from any constraint variant,
1363    /// providing a uniform way to access the constraint specification regardless
1364    /// of its type classification.
1365    ///
1366    /// # Returns
1367    ///
1368    /// Returns `&str` containing the original constraint string.
1369    ///
1370    /// # Examples
1371    ///
1372    /// ```rust,no_run
1373    /// use agpm_cli::version::VersionConstraint;
1374    ///
1375    /// let tag = VersionConstraint::Tag("^1.0.0".to_string());
1376    /// assert_eq!(tag.as_str(), "^1.0.0");
1377    ///
1378    /// let branch = VersionConstraint::Branch("feature/auth".to_string());
1379    /// assert_eq!(branch.as_str(), "feature/auth");
1380    ///
1381    /// let commit = VersionConstraint::Commit("abc123def456".to_string());
1382    /// assert_eq!(commit.as_str(), "abc123def456");
1383    /// ```
1384    ///
1385    /// # Use Cases
1386    ///
1387    /// This method is useful for:
1388    /// - Displaying constraints in user interfaces
1389    /// - Logging and debugging version resolution
1390    /// - Passing constraint strings to external tools
1391    /// - Serializing constraints to text formats
1392    #[must_use]
1393    pub fn as_str(&self) -> &str {
1394        match self {
1395            Self::Tag(s) => s,
1396            Self::Branch(s) => s,
1397            Self::Commit(s) => s,
1398        }
1399    }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404    use super::*;
1405    use crate::test_utils::TestGit;
1406    use tempfile::TempDir;
1407
1408    fn create_test_repo_with_tags() -> (TempDir, GitRepo) {
1409        let temp_dir = TempDir::new().unwrap();
1410        let repo_path = temp_dir.path();
1411
1412        // Use TestGit helper instead of raw Command
1413        let git = TestGit::new(repo_path);
1414        git.init().unwrap();
1415        git.config_user().unwrap();
1416
1417        std::fs::write(repo_path.join("README.md"), "Test").unwrap();
1418
1419        git.add_all().unwrap();
1420        git.commit("Initial commit").unwrap();
1421
1422        let tags = vec!["v1.0.0", "v1.1.0", "v1.2.0", "v2.0.0-beta.1", "v2.0.0"];
1423        for tag in tags {
1424            git.tag(tag).unwrap();
1425        }
1426
1427        let repo = GitRepo::new(repo_path);
1428        (temp_dir, repo)
1429    }
1430
1431    #[tokio::test]
1432    async fn test_version_parsing() {
1433        let (_temp, repo) = create_test_repo_with_tags();
1434        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1435
1436        assert_eq!(resolver.versions.len(), 5);
1437        assert_eq!(resolver.get_latest().unwrap().tag, "v2.0.0");
1438        assert_eq!(resolver.get_latest_stable().unwrap().tag, "v2.0.0");
1439    }
1440
1441    #[tokio::test]
1442    async fn test_version_resolution() {
1443        let (_temp, repo) = create_test_repo_with_tags();
1444        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1445
1446        // Exact versions
1447        assert_eq!(resolver.resolve("1.1.0").unwrap().unwrap().tag, "v1.1.0");
1448        assert_eq!(resolver.resolve("v1.1.0").unwrap().unwrap().tag, "v1.1.0");
1449
1450        // Version constraints
1451        assert_eq!(resolver.resolve("^1.0.0").unwrap().unwrap().tag, "v1.2.0");
1452        assert_eq!(resolver.resolve("~1.1.0").unwrap().unwrap().tag, "v1.1.0");
1453    }
1454
1455    #[tokio::test]
1456    async fn test_has_version() {
1457        let (_temp, repo) = create_test_repo_with_tags();
1458        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1459
1460        // Exact versions
1461        assert!(resolver.has_version("v1.0.0"));
1462        assert!(resolver.has_version("1.0.0"));
1463
1464        // Non-existent versions (including "latest" - just a tag name)
1465        assert!(!resolver.has_version("v3.0.0"));
1466        assert!(!resolver.has_version("latest"));
1467        assert!(!resolver.has_version("latest-prerelease"));
1468    }
1469
1470    #[tokio::test]
1471    async fn test_matches_requirement() {
1472        // Unprefixed versions
1473        assert!(matches_requirement("1.2.0", "^1.0.0").unwrap());
1474        assert!(matches_requirement("v1.2.0", "^1.0.0").unwrap());
1475        assert!(!matches_requirement("2.0.0", "^1.0.0").unwrap());
1476        assert!(matches_requirement("any.version", "*").unwrap());
1477    }
1478
1479    #[test]
1480    fn test_matches_requirement_with_prefixes() {
1481        // Prefixed versions with matching prefixes
1482        assert!(matches_requirement("agents-v1.2.0", "agents-^v1.0.0").unwrap());
1483        assert!(matches_requirement("agents-v1.2.5", "agents-~v1.2.0").unwrap());
1484        assert!(matches_requirement("tool123-v2.0.0", "tool123->=v1.0.0").unwrap());
1485
1486        // Prefixed versions with wildcard
1487        assert!(matches_requirement("agents-v1.2.0", "agents-*").unwrap());
1488
1489        // Prefixed version doesn't match unprefixed requirement
1490        assert!(!matches_requirement("agents-v1.2.0", "^v1.0.0").unwrap());
1491
1492        // Unprefixed version doesn't match prefixed requirement
1493        assert!(!matches_requirement("v1.2.0", "agents-^v1.0.0").unwrap());
1494
1495        // Different prefixes don't match
1496        assert!(!matches_requirement("agents-v1.2.0", "snippets-^v1.0.0").unwrap());
1497        assert!(!matches_requirement("tool-v1.0.0", "agent-v1.0.0").unwrap());
1498
1499        // Prefixed version doesn't satisfy constraint
1500        assert!(!matches_requirement("agents-v2.0.0", "agents-^v1.0.0").unwrap());
1501    }
1502
1503    #[test]
1504    fn test_parse_version_constraint() {
1505        // Unprefixed constraints
1506        assert_eq!(
1507            parse_version_constraint("v1.0.0"),
1508            VersionConstraint::Tag("v1.0.0".to_string())
1509        );
1510        assert_eq!(
1511            parse_version_constraint("^1.0.0"),
1512            VersionConstraint::Tag("^1.0.0".to_string())
1513        );
1514        assert_eq!(parse_version_constraint("*"), VersionConstraint::Tag("*".to_string()));
1515        assert_eq!(parse_version_constraint("main"), VersionConstraint::Branch("main".to_string()));
1516        assert_eq!(
1517            parse_version_constraint("latest"),
1518            VersionConstraint::Branch("latest".to_string())
1519        );
1520        assert_eq!(
1521            parse_version_constraint("latest-prerelease"),
1522            VersionConstraint::Branch("latest-prerelease".to_string())
1523        );
1524        assert_eq!(
1525            parse_version_constraint("feature/test"),
1526            VersionConstraint::Branch("feature/test".to_string())
1527        );
1528        assert_eq!(
1529            parse_version_constraint("abc1234"),
1530            VersionConstraint::Commit("abc1234".to_string())
1531        );
1532        assert_eq!(
1533            parse_version_constraint("1234567890abcdef"),
1534            VersionConstraint::Commit("1234567890abcdef".to_string())
1535        );
1536
1537        // Prefixed constraints - all should be Tag
1538        assert_eq!(
1539            parse_version_constraint("agents-v1.2.0"),
1540            VersionConstraint::Tag("agents-v1.2.0".to_string())
1541        );
1542        assert_eq!(
1543            parse_version_constraint("agents-^v1.0.0"),
1544            VersionConstraint::Tag("agents-^v1.0.0".to_string())
1545        );
1546        assert_eq!(
1547            parse_version_constraint("snippets-~v2.0.0"),
1548            VersionConstraint::Tag("snippets-~v2.0.0".to_string())
1549        );
1550        assert_eq!(
1551            parse_version_constraint("tool123-*"),
1552            VersionConstraint::Tag("tool123-*".to_string())
1553        );
1554        assert_eq!(
1555            parse_version_constraint("my-cool-tool->=v1.0.0"),
1556            VersionConstraint::Tag("my-cool-tool->=v1.0.0".to_string())
1557        );
1558
1559        // Prefixed branches should still be Branch
1560        assert_eq!(
1561            parse_version_constraint("agents-main"),
1562            VersionConstraint::Branch("agents-main".to_string())
1563        );
1564    }
1565
1566    #[tokio::test]
1567    async fn test_version_list_all() {
1568        let (_temp, repo) = create_test_repo_with_tags();
1569        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1570
1571        let all_versions = resolver.list_all();
1572        assert_eq!(all_versions.len(), 5);
1573
1574        // Should be sorted in descending order
1575        assert_eq!(all_versions[0].tag, "v2.0.0");
1576        assert_eq!(all_versions[1].tag, "v2.0.0-beta.1");
1577    }
1578
1579    #[tokio::test]
1580    async fn test_version_list_stable() {
1581        let (_temp, repo) = create_test_repo_with_tags();
1582        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1583
1584        let stable_versions = resolver.list_stable();
1585        assert_eq!(stable_versions.len(), 4); // No beta versions
1586
1587        for version in stable_versions {
1588            assert!(!version.prerelease);
1589        }
1590    }
1591
1592    // ========== Prefix Support Tests ==========
1593
1594    #[test]
1595    fn test_split_prefix_and_version() {
1596        // Prefixed versions
1597        assert_eq!(
1598            split_prefix_and_version("agents-v1.0.0"),
1599            (Some("agents".to_string()), "v1.0.0")
1600        );
1601        assert_eq!(
1602            split_prefix_and_version("agents-^v1.0.0"),
1603            (Some("agents".to_string()), "^v1.0.0")
1604        );
1605        assert_eq!(
1606            split_prefix_and_version("my-cool-agent-v2.0.0"),
1607            (Some("my-cool-agent".to_string()), "v2.0.0")
1608        );
1609
1610        // Unprefixed versions
1611        assert_eq!(split_prefix_and_version("v1.0.0"), (None, "v1.0.0"));
1612        assert_eq!(split_prefix_and_version("^v1.0.0"), (None, "^v1.0.0"));
1613        assert_eq!(split_prefix_and_version("1.0.0"), (None, "1.0.0"));
1614
1615        // Edge cases
1616        assert_eq!(
1617            split_prefix_and_version("tool-v-v1.0.0"),
1618            (Some("tool-v".to_string()), "v1.0.0")
1619        );
1620        assert_eq!(split_prefix_and_version("a-b-c-v1.0.0"), (Some("a-b-c".to_string()), "v1.0.0"));
1621        assert_eq!(
1622            split_prefix_and_version("prefix-~1.0.0"),
1623            (Some("prefix".to_string()), "~1.0.0")
1624        );
1625    }
1626
1627    #[test]
1628    fn test_split_prefix_edge_cases() {
1629        // Empty prefix - should be treated as None
1630        assert_eq!(split_prefix_and_version("-v1.0.0"), (None, "v1.0.0"));
1631        assert_eq!(split_prefix_and_version("--v1.0.0"), (None, "v1.0.0"));
1632
1633        // Prefix with numbers - digits in middle of prefix are preserved
1634        assert_eq!(
1635            split_prefix_and_version("tool123-v1.0.0"),
1636            (Some("tool123".to_string()), "v1.0.0")
1637        );
1638        assert_eq!(
1639            split_prefix_and_version("agent2-v1.0.0"),
1640            (Some("agent2".to_string()), "v1.0.0")
1641        );
1642        // Digit after hyphen is treated as version start
1643        assert_eq!(split_prefix_and_version("tool-123"), (Some("tool".to_string()), "123"));
1644        // 'v' followed by digit takes precedence
1645        assert_eq!(
1646            split_prefix_and_version("abc-v2-agent-v1.0.0"),
1647            (Some("abc".to_string()), "v2-agent-v1.0.0")
1648        );
1649
1650        // Very long prefix (stress test)
1651        let long_prefix = "a".repeat(100);
1652        let tag = format!("{}-v1.0.0", long_prefix);
1653        let (prefix, version) = split_prefix_and_version(&tag);
1654        assert_eq!(prefix, Some(long_prefix));
1655        assert_eq!(version, "v1.0.0");
1656
1657        // Unicode in prefixes - note: 'v' followed by digit is detected as version
1658        assert_eq!(
1659            split_prefix_and_version("агенты-v1.0.0"),
1660            (Some("агенты".to_string()), "v1.0.0")
1661        );
1662        assert_eq!(split_prefix_and_version("工具-v1.0.0"), (Some("工具".to_string()), "v1.0.0"));
1663        // Unicode prefixes without version pattern
1664        assert_eq!(split_prefix_and_version("агенты-2.0.0"), (Some("агенты".to_string()), "2.0.0"));
1665
1666        // String ending with 'v' (tests panic fix)
1667        assert_eq!(split_prefix_and_version("prefix-v"), (None, "prefix-v"));
1668        assert_eq!(split_prefix_and_version("v"), (None, "v"));
1669
1670        // Multiple hyphens
1671        assert_eq!(
1672            split_prefix_and_version("my-cool-tool-v1.0.0"),
1673            (Some("my-cool-tool".to_string()), "v1.0.0")
1674        );
1675    }
1676
1677    fn create_test_repo_with_prefixed_tags() -> (TempDir, GitRepo) {
1678        let temp_dir = TempDir::new().unwrap();
1679        let repo_path = temp_dir.path();
1680
1681        // Use TestGit helper instead of raw Command
1682        let git = TestGit::new(repo_path);
1683        git.init().unwrap();
1684        git.config_user().unwrap();
1685
1686        std::fs::write(repo_path.join("README.md"), "Test").unwrap();
1687
1688        git.add_all().unwrap();
1689        git.commit("Initial commit").unwrap();
1690
1691        // Create tags with different prefixes
1692        let tags = vec![
1693            "agents-v1.0.0",
1694            "agents-v1.2.0",
1695            "agents-v2.0.0",
1696            "snippets-v1.0.0",
1697            "snippets-v1.5.0",
1698            "v1.0.0", // Unprefixed
1699            "v2.0.0", // Unprefixed
1700        ];
1701        for tag in tags {
1702            git.tag(tag).unwrap();
1703        }
1704
1705        let repo = GitRepo::new(repo_path);
1706        (temp_dir, repo)
1707    }
1708
1709    #[tokio::test]
1710    async fn test_prefixed_version_parsing() {
1711        let (_temp, repo) = create_test_repo_with_prefixed_tags();
1712        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1713
1714        // Should parse all 7 tags
1715        assert_eq!(resolver.versions.len(), 7);
1716
1717        // Check prefixes are correctly extracted
1718        let agents_versions: Vec<_> =
1719            resolver.versions.iter().filter(|v| v.prefix == Some("agents".to_string())).collect();
1720        assert_eq!(agents_versions.len(), 3);
1721
1722        let snippets_versions: Vec<_> =
1723            resolver.versions.iter().filter(|v| v.prefix == Some("snippets".to_string())).collect();
1724        assert_eq!(snippets_versions.len(), 2);
1725
1726        let unprefixed_versions: Vec<_> =
1727            resolver.versions.iter().filter(|v| v.prefix.is_none()).collect();
1728        assert_eq!(unprefixed_versions.len(), 2);
1729    }
1730
1731    #[tokio::test]
1732    async fn test_prefixed_version_resolution() {
1733        let (_temp, repo) = create_test_repo_with_prefixed_tags();
1734        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1735
1736        // Prefixed exact version
1737        let result = resolver.resolve("agents-v1.2.0").unwrap().unwrap();
1738        assert_eq!(result.tag, "agents-v1.2.0");
1739        assert_eq!(result.prefix, Some("agents".to_string()));
1740
1741        // Prefixed constraint - should match highest agents version
1742        let result = resolver.resolve("agents-^v1.0.0").unwrap().unwrap();
1743        assert_eq!(result.tag, "agents-v1.2.0");
1744        assert_eq!(result.prefix, Some("agents".to_string()));
1745
1746        // Different prefix constraint
1747        let result = resolver.resolve("snippets-^v1.0.0").unwrap().unwrap();
1748        assert_eq!(result.tag, "snippets-v1.5.0");
1749        assert_eq!(result.prefix, Some("snippets".to_string()));
1750
1751        // Unprefixed constraint should only match unprefixed tags
1752        let result = resolver.resolve("^v1.0.0").unwrap().unwrap();
1753        assert_eq!(result.tag, "v1.0.0");
1754        assert_eq!(result.prefix, None);
1755    }
1756
1757    #[tokio::test]
1758    async fn test_prefix_isolation() {
1759        let (_temp, repo) = create_test_repo_with_prefixed_tags();
1760        let resolver = VersionResolver::from_git_tags(&repo).await.unwrap();
1761
1762        // agents-^v1.0.0 should NOT match snippets-v1.5.0 even though 1.5.0 > 1.0.0
1763        let result = resolver.resolve("agents-^v1.0.0").unwrap().unwrap();
1764        assert_eq!(result.prefix, Some("agents".to_string()));
1765        assert_ne!(result.tag, "snippets-v1.5.0");
1766
1767        // Unprefixed constraint should NOT match prefixed tags
1768        let result = resolver.resolve("^v1.0.0").unwrap().unwrap();
1769        assert_eq!(result.prefix, None);
1770        assert!(!result.tag.contains("agents-"));
1771        assert!(!result.tag.contains("snippets-"));
1772    }
1773
1774    #[test]
1775    fn test_parse_version_req_with_prefix() -> anyhow::Result<()> {
1776        // The parse_version_req function should work on the version part only
1777        parse_version_req("^1.0.0")?;
1778        parse_version_req("^v1.0.0")?;
1779        parse_version_req("~2.1.0")?;
1780        parse_version_req(">=1.0.0")?;
1781        Ok(())
1782    }
1783}