agpm_cli/resolver/
redundancy.rs

1//! Redundancy detection and analysis for AGPM dependencies.
2//!
3//! This module provides sophisticated analysis of dependency redundancy patterns
4//! to help users optimize their manifest files and understand resource usage.
5//! Redundancy detection is designed to be advisory rather than blocking,
6//! enabling legitimate use cases while highlighting optimization opportunities.
7//!
8//! # Types of Redundancy
9//!
10//! ## Version Redundancy
11//! Multiple resources referencing the same source file with different versions:
12//! ```toml
13//! [agents]
14//! app-helper = { source = "community", path = "agents/helper.md", version = "v1.0.0" }
15//! tool-helper = { source = "community", path = "agents/helper.md", version = "v2.0.0" }
16//! ```
17//!
18//! ## Mixed Constraint Redundancy  
19//! Some dependencies use specific versions while others use latest:
20//! ```toml
21//! [agents]
22//! main-agent = { source = "community", path = "agents/helper.md" } # latest
23//! backup-agent = { source = "community", path = "agents/helper.md", version = "v1.0.0" }
24//! ```
25//!
26//! ## Cross-Source Redundancy (Future)
27//! Same resource available from multiple sources (not yet implemented):
28//! ```toml
29//! [sources]
30//! official = "https://github.com/org/agpm-official.git"
31//! mirror = "https://github.com/org/agpm-mirror.git"
32//!
33//! [agents]
34//! helper1 = { source = "official", path = "agents/helper.md" }
35//! helper2 = { source = "mirror", path = "agents/helper.md" }
36//! ```
37//!
38//! # Algorithm Design
39//!
40//! The redundancy detection algorithm operates in O(n) time complexity:
41//! 1. **Collection Phase**: Build usage map of source files → resources (O(n))
42//! 2. **Analysis Phase**: Identify files with multiple version usages (O(n))
43//! 3. **Classification Phase**: Categorize redundancy types (O(k) where k = redundancies)
44//!
45//! ## Data Structures
46//!
47//! The detector uses a hash map for efficient lookup:
48//! ```text
49//! usages: HashMap<String, Vec<ResourceUsage>>
50//!         ↑                ↑
51//!         source:path      list of resources using this file
52//! ```
53//!
54//! # Design Principles
55//!
56//! ## Non-Blocking Detection
57//! Redundancy analysis never prevents installation because:
58//! - **A/B Testing**: Users may intentionally install multiple versions
59//! - **Gradual Migration**: Transitioning between versions may require temporary redundancy
60//! - **Testing Environments**: Different test scenarios may need different versions
61//! - **Rollback Capability**: Keeping previous versions enables quick rollbacks
62//!
63//! ## Helpful Suggestions
64//! Instead of blocking, the detector provides:
65//! - **Version Alignment**: Suggest using consistent versions across resources
66//! - **Consolidation Opportunities**: Identify resources that could share versions
67//! - **Best Practices**: Guide users toward maintainable dependency patterns
68//!
69//! # Performance Considerations
70//!
71//! - **Lazy Evaluation**: Analysis only runs when explicitly requested
72//! - **Memory Efficient**: Uses references where possible to avoid cloning
73//! - **Early Termination**: Stops processing once redundancies are found (for boolean checks)
74//! - **Batched Operations**: Groups related analysis operations together
75//!
76//! # Future Extensions
77//!
78//! Planned enhancements for redundancy detection:
79//!
80//! ## Transitive Analysis
81//! When dependencies-of-dependencies are supported:
82//! ```text
83//! impl RedundancyDetector {
84//!     pub fn check_transitive_redundancies(&self) -> Vec<Redundancy> {
85//!         // Analyze entire dependency tree for redundant patterns
86//!     }
87//! }
88//! ```
89//!
90//! ## Content-Based Detection
91//! Hash-based redundancy detection for identical files:
92//! ```rust,no_run
93//! # use agpm_cli::resolver::redundancy::ResourceUsage;
94//! pub struct ContentRedundancy {
95//!     content_hash: String,
96//!     identical_resources: Vec<ResourceUsage>,
97//! }
98//! ```
99//!
100//! ## Semantic Analysis
101//! ML-based detection of functionally similar resources:
102//! ```rust,no_run
103//! # use agpm_cli::resolver::redundancy::ResourceUsage;
104//! pub struct SemanticRedundancy {
105//!     similarity_score: f64,
106//!     similar_resources: Vec<ResourceUsage>,
107//! }
108//! ```
109
110use crate::manifest::{Manifest, ResourceDependency};
111use colored::Colorize;
112use std::collections::{HashMap, HashSet};
113use std::fmt;
114
115/// Represents a specific usage of a source file by a resource dependency.
116///
117/// This struct captures how a particular resource (agent or snippet) uses
118/// a source file, including version constraints and naming information.
119/// It's the fundamental unit of redundancy analysis.
120///
121/// # Fields
122///
123/// - `resource_name`: The name given to this resource in the manifest
124/// - `source_file`: Composite identifier in format "source:path"
125/// - `version`: Version constraint (None means latest/default branch)
126///
127/// # Example
128///
129/// For this manifest entry:
130/// ```toml
131/// [agents]
132/// my-helper = { source = "community", path = "agents/helper.md", version = "v1.2.3" }
133/// ```
134///
135/// The corresponding `ResourceUsage` would be:
136/// ```rust,no_run
137/// # use agpm_cli::resolver::redundancy::ResourceUsage;
138/// ResourceUsage {
139///     resource_name: "my-helper".to_string(),
140///     source_file: "community:agents/helper.md".to_string(),
141///     version: Some("v1.2.3".to_string()),
142/// }
143/// # ;
144/// ```
145#[derive(Debug, Clone)]
146pub struct ResourceUsage {
147    /// The resource name that uses this source file
148    pub resource_name: String,
149    /// The source file being used (source:path)
150    pub source_file: String,
151    /// The version being used
152    pub version: Option<String>,
153}
154
155impl fmt::Display for ResourceUsage {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(
158            f,
159            "'{}' uses version {}",
160            self.resource_name,
161            self.version.as_deref().unwrap_or("latest")
162        )
163    }
164}
165
166/// Represents a detected redundancy pattern where multiple resources use the same source file.
167///
168/// A [`Redundancy`] is created when the analysis detects that multiple resources
169/// reference the same source file (identified by source:path) but with different
170/// version constraints or names.
171///
172/// # Redundancy Criteria
173///
174/// A redundancy is detected when:
175/// 1. **Multiple Usages**: More than one resource uses the same source file
176/// 2. **Version Differences**: The usages specify different version constraints
177///
178/// Note: Multiple resources using the same source file with identical versions
179/// are NOT considered redundant, as this is a valid use case.
180///
181/// # Use Cases for Legitimate Redundancy
182///
183/// - **A/B Testing**: Installing multiple versions for comparison
184/// - **Migration Periods**: Gradually transitioning between versions
185/// - **Rollback Preparation**: Keeping previous versions for quick rollback
186/// - **Environment Differences**: Different versions for dev/staging/prod
187///
188/// # Display Format
189///
190/// When displayed, redundancies show:
191/// ```text
192/// ⚠ Multiple versions of 'community:agents/helper.md' will be installed:
193///   - 'app-helper' uses version v1.0.0
194///   - 'tool-helper' uses version v2.0.0
195/// ```
196#[derive(Debug)]
197pub struct Redundancy {
198    /// The source file that is used multiple times
199    pub source_file: String,
200    /// All usages of this source file
201    pub usages: Vec<ResourceUsage>,
202}
203
204impl fmt::Display for Redundancy {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        writeln!(
207            f,
208            "{} Multiple versions of '{}' will be installed:",
209            "⚠".yellow(),
210            self.source_file
211        )?;
212        for usage in &self.usages {
213            writeln!(f, "  - {usage}")?;
214        }
215        Ok(())
216    }
217}
218
219/// Analyzes dependency patterns to detect and categorize redundancies.
220///
221/// The [`RedundancyDetector`] is the main analysis engine for identifying
222/// optimization opportunities in dependency manifests. It builds a comprehensive
223/// view of how resources use source files and identifies patterns that might
224/// indicate redundancy.
225///
226/// # Analysis Process
227///
228/// 1. **Collection**: Gather all resource usages via [`add_usage()`] or [`analyze_manifest()`]
229/// 2. **Detection**: Run [`detect_redundancies()`] to find redundant patterns
230/// 3. **Reporting**: Generate warnings or suggestions using helper methods
231///
232/// # Thread Safety
233///
234/// The detector is not thread-safe due to mutable state during analysis.
235/// Create separate instances for concurrent analysis operations.
236///
237/// # Memory Usage
238///
239/// The detector maintains an in-memory map of all resource usages. For large
240/// manifests with hundreds of dependencies, memory usage scales linearly:
241/// - Each resource usage: ~100 bytes (strings + metadata)
242/// - `HashMap` overhead: ~25% of total usage data
243///
244/// [`add_usage()`]: RedundancyDetector::add_usage
245/// [`analyze_manifest()`]: RedundancyDetector::analyze_manifest
246/// [`detect_redundancies()`]: RedundancyDetector::detect_redundancies
247pub struct RedundancyDetector {
248    /// Map of source file identifiers to their usages
249    usages: HashMap<String, Vec<ResourceUsage>>,
250}
251
252impl Default for RedundancyDetector {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258impl RedundancyDetector {
259    /// Creates a new redundancy detector with empty state.
260    ///
261    /// The detector starts with no resource usage data. Use [`add_usage()`]
262    /// for individual dependencies or [`analyze_manifest()`] for complete
263    /// manifest analysis.
264    ///
265    /// # Example
266    ///
267    /// ```rust,no_run
268    /// use agpm_cli::resolver::redundancy::RedundancyDetector;
269    ///
270    /// let mut detector = RedundancyDetector::new();
271    /// // Add usages or analyze manifest...
272    /// let redundancies = detector.detect_redundancies();
273    /// ```
274    ///
275    /// [`add_usage()`]: RedundancyDetector::add_usage
276    /// [`analyze_manifest()`]: RedundancyDetector::analyze_manifest
277    #[must_use]
278    pub fn new() -> Self {
279        Self {
280            usages: HashMap::new(),
281        }
282    }
283
284    /// Records a resource usage for redundancy analysis.
285    ///
286    /// This method adds a single resource dependency to the analysis dataset.
287    /// Local dependencies are automatically filtered out since they don't
288    /// have redundancy concerns (each local path is unique).
289    ///
290    /// # Filtering Logic
291    ///
292    /// - **Remote Dependencies**: Added to analysis (have source + path)
293    /// - **Local Dependencies**: Skipped (path-only, no redundancy issues)
294    /// - **Invalid Dependencies**: Skipped (missing source information)
295    ///
296    /// # Source File Identification
297    ///
298    /// Remote dependencies are identified by their composite key:
299    /// ```text
300    /// source_file = "{source_name}:{resource_path}"
301    /// ```
302    ///
303    /// This ensures that the same file from different sources is treated
304    /// as separate resources (no cross-source redundancy detection yet).
305    ///
306    /// # Parameters
307    ///
308    /// - `resource_name`: Name assigned to this resource in the manifest
309    /// - `dep`: Resource dependency specification from manifest
310    ///
311    /// # Example
312    ///
313    /// ```rust,no_run
314    /// use agpm_cli::resolver::redundancy::RedundancyDetector;
315    /// use agpm_cli::manifest::{ResourceDependency, DetailedDependency};
316    ///
317    /// let mut detector = RedundancyDetector::new();
318    ///
319    /// // This will be recorded
320    /// let remote_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
321    ///     source: Some("community".to_string()),
322    ///     path: "agents/helper.md".to_string(),
323    ///     version: Some("v1.0.0".to_string()),
324    ///     branch: None,
325    ///     rev: None,
326    ///     command: None,
327    ///     args: None,
328    ///     target: None,
329    ///     filename: None,
330    ///     dependencies: None,
331    ///     tool: "claude-code".to_string(),
332    /// }));
333    /// detector.add_usage("my-helper".to_string(), &remote_dep);
334    ///
335    /// // This will be ignored (local dependency)
336    /// let local_dep = ResourceDependency::Simple("../local/helper.md".to_string());
337    /// detector.add_usage("local-helper".to_string(), &local_dep);
338    /// ```
339    pub fn add_usage(&mut self, resource_name: String, dep: &ResourceDependency) {
340        // Only track remote dependencies (local ones don't have redundancy issues)
341        if dep.is_local() {
342            return;
343        }
344
345        if let Some(source) = dep.get_source() {
346            let source_file = format!("{}:{}", source, dep.get_path());
347
348            let usage = ResourceUsage {
349                resource_name,
350                source_file: source_file.clone(),
351                version: dep.get_version().map(std::string::ToString::to_string),
352            };
353
354            self.usages.entry(source_file).or_default().push(usage);
355        }
356    }
357
358    /// Analyzes all dependencies from a manifest for redundancy patterns.
359    ///
360    /// This is a convenience method that processes all dependencies from
361    /// a manifest file in a single operation. It's equivalent to calling
362    /// [`add_usage()`] for each dependency individually.
363    ///
364    /// # Processing Scope
365    ///
366    /// The method analyzes:
367    /// - **Agent Dependencies**: From `[agents]` section
368    /// - **Snippet Dependencies**: From `[snippets]` section
369    /// - **Remote Dependencies**: Only those with source specifications
370    ///
371    /// Local dependencies are automatically filtered out during analysis.
372    ///
373    /// # Usage Pattern
374    ///
375    /// This method is typically used in the main resolution workflow:
376    /// ```rust,no_run
377    /// use agpm_cli::resolver::redundancy::RedundancyDetector;
378    /// use agpm_cli::manifest::Manifest;
379    /// use std::path::Path;
380    ///
381    /// # fn example() -> anyhow::Result<()> {
382    /// let manifest = Manifest::load(Path::new("agpm.toml"))?;
383    /// let mut detector = RedundancyDetector::new();
384    /// detector.analyze_manifest(&manifest);
385    ///
386    /// let redundancies = detector.detect_redundancies();
387    /// let warning = detector.generate_redundancy_warning(&redundancies);
388    /// if !warning.is_empty() {
389    ///     eprintln!("{}", warning);
390    /// }
391    /// # Ok(())
392    /// # }
393    /// ```
394    ///
395    /// # Performance
396    ///
397    /// - **Time Complexity**: O(n) where n = total dependencies
398    /// - **Space Complexity**: O(r) where r = remote dependencies
399    /// - **Memory Usage**: Linear with number of remote dependencies
400    ///
401    /// [`add_usage()`]: RedundancyDetector::add_usage
402    pub fn analyze_manifest(&mut self, manifest: &Manifest) {
403        for (name, dep) in manifest.all_dependencies() {
404            self.add_usage(name.to_string(), dep);
405        }
406    }
407
408    /// Detects redundancy patterns in the collected resource usages.
409    ///
410    /// This method analyzes all collected resource usages and identifies
411    /// patterns where multiple resources use the same source file with
412    /// different version constraints.
413    ///
414    /// # Detection Algorithm
415    ///
416    /// For each source file in the usage map:
417    /// 1. **Skip Single Usage**: Files used by only one resource are not redundant
418    /// 2. **Version Analysis**: Collect all unique version constraints for the file
419    /// 3. **Redundancy Check**: If multiple different versions exist, mark as redundant
420    ///
421    /// # Redundancy Criteria
422    ///
423    /// A source file is considered redundant when:
424    /// - **Multiple Resources**: More than one resource uses the file
425    /// - **Different Versions**: Resources specify different version constraints
426    ///
427    /// # Non-Redundant Cases
428    ///
429    /// These cases are NOT considered redundant:
430    /// - Single resource using a source file
431    /// - Multiple resources using identical version constraints
432    /// - Multiple resources all using "latest" (no version specified)
433    ///
434    /// # Algorithm Complexity
435    ///
436    /// - **Time**: O(n + k·m) where:
437    ///   - n = total resource usages
438    ///   - k = unique source files
439    ///   - m = average usages per file
440    /// - **Space**: O(r) where r = detected redundancies
441    ///
442    /// # Returns
443    ///
444    /// A vector of [`Redundancy`] objects, each representing a source file
445    /// with redundant usage patterns. The vector is empty if no redundancies
446    /// are detected.
447    ///
448    /// # Example Output
449    ///
450    /// For a manifest with redundant dependencies, this method might return:
451    /// ```text
452    /// [
453    ///     Redundancy {
454    ///         source_file: "community:agents/helper.md",
455    ///         usages: [
456    ///             ResourceUsage { resource_name: "app-helper", version: Some("v1.0.0") },
457    ///             ResourceUsage { resource_name: "tool-helper", version: Some("v2.0.0") },
458    ///         ]
459    ///     }
460    /// ]
461    /// ```
462    ///
463    /// [`Redundancy`]: Redundancy
464    #[must_use]
465    pub fn detect_redundancies(&self) -> Vec<Redundancy> {
466        let mut redundancies = Vec::new();
467
468        for (source_file, uses) in &self.usages {
469            // Skip if only one resource uses this source file
470            if uses.len() <= 1 {
471                continue;
472            }
473
474            // Collect unique versions
475            let versions: HashSet<Option<String>> =
476                uses.iter().map(|u| u.version.clone()).collect();
477
478            // If there are different versions, it's a redundancy worth noting
479            if versions.len() > 1 {
480                redundancies.push(Redundancy {
481                    source_file: source_file.clone(),
482                    usages: uses.clone(),
483                });
484            }
485        }
486
487        redundancies
488    }
489
490    /// Determines if a redundancy could be consolidated to use a single version.
491    ///
492    /// This method analyzes a detected redundancy to determine if all resources
493    /// using the source file could reasonably be updated to use the same version.
494    /// This is a heuristic for suggesting consolidation opportunities.
495    ///
496    /// # Consolidation Logic
497    ///
498    /// A redundancy can be consolidated if:
499    /// - All resources use the same version constraint (already consolidated)
500    /// - All resources use "latest" (no specific versions)
501    ///
502    /// A redundancy cannot be easily consolidated if:
503    /// - Resources use different specific versions (may have compatibility reasons)
504    /// - Mixed latest and specific versions (may indicate intentional pinning)
505    ///
506    /// # Use Cases
507    ///
508    /// This method helps identify:
509    /// - **Easy Wins**: Redundancies that could be quickly resolved
510    /// - **Complex Cases**: Redundancies that may require careful consideration
511    /// - **Intentional Patterns**: Cases where redundancy might be deliberate
512    ///
513    /// # Parameters
514    ///
515    /// - `redundancy`: The redundancy pattern to analyze
516    ///
517    /// # Returns
518    ///
519    /// - `true`: All usages could likely be consolidated to a single version
520    /// - `false`: Consolidation would require careful analysis of compatibility
521    ///
522    /// # Example
523    ///
524    /// ```rust,no_run
525    /// use agpm_cli::resolver::redundancy::{RedundancyDetector, Redundancy, ResourceUsage};
526    ///
527    /// let detector = RedundancyDetector::new();
528    ///
529    /// // Easy to consolidate (all use latest)
530    /// let easy_redundancy = Redundancy {
531    ///     source_file: "community:agents/helper.md".to_string(),
532    ///     usages: vec![
533    ///         ResourceUsage { resource_name: "helper1".to_string(), source_file: "community:agents/helper.md".to_string(), version: None },
534    ///         ResourceUsage { resource_name: "helper2".to_string(), source_file: "community:agents/helper.md".to_string(), version: None },
535    ///     ]
536    /// };
537    /// assert!(detector.can_consolidate(&easy_redundancy));
538    ///
539    /// // Hard to consolidate (different versions)
540    /// let hard_redundancy = Redundancy {
541    ///     source_file: "community:agents/helper.md".to_string(),
542    ///     usages: vec![
543    ///         ResourceUsage { resource_name: "helper1".to_string(), source_file: "community:agents/helper.md".to_string(), version: Some("v1.0.0".to_string()) },
544    ///         ResourceUsage { resource_name: "helper2".to_string(), source_file: "community:agents/helper.md".to_string(), version: Some("v2.0.0".to_string()) },
545    ///     ]
546    /// };
547    /// assert!(!detector.can_consolidate(&hard_redundancy));
548    /// ```
549    #[must_use]
550    pub fn can_consolidate(&self, redundancy: &Redundancy) -> bool {
551        // If all usages want the same version or latest, they could be consolidated
552        let versions: HashSet<_> = redundancy.usages.iter().map(|u| &u.version).collect();
553        versions.len() == 1
554    }
555
556    /// Generates a comprehensive warning message for detected redundancies.
557    ///
558    /// This method creates a user-friendly warning message that explains detected
559    /// redundancies and provides actionable suggestions for optimization. The
560    /// message is designed to be informative rather than alarming, emphasizing
561    /// that redundancy is not an error.
562    ///
563    /// # Message Structure
564    ///
565    /// The generated warning includes:
566    /// 1. **Header**: Clear indication this is a warning, not an error
567    /// 2. **Redundancy List**: Each detected redundancy with details
568    /// 3. **General Guidance**: Explanation of implications and options
569    /// 4. **Specific Suggestions**: Targeted advice based on detected patterns
570    ///
571    /// # Message Tone
572    ///
573    /// The warning message maintains a helpful, non-blocking tone:
574    /// - Emphasizes that installation will proceed normally
575    /// - Explains that redundancy may be intentional
576    /// - Provides optimization suggestions without mandating changes
577    /// - Uses clear, jargon-free language
578    ///
579    /// # Color Coding
580    ///
581    /// The message uses terminal colors for better readability:
582    /// - **Yellow**: Warning indicators and attention markers
583    /// - **Blue**: Informational notes and suggestions
584    /// - **Default**: Main content and resource names
585    ///
586    /// # Parameters
587    ///
588    /// - `redundancies`: List of detected redundancy patterns
589    ///
590    /// # Returns
591    ///
592    /// - **Non-empty**: Formatted warning message if redundancies exist
593    /// - **Empty string**: If no redundancies provided
594    ///
595    /// # Example Output
596    ///
597    /// ```text
598    /// Warning: Redundant dependencies detected
599    ///
600    /// ⚠ Multiple versions of 'community:agents/helper.md' will be installed:
601    ///   - 'app-helper' uses version v1.0.0
602    ///   - 'tool-helper' uses version latest
603    ///
604    /// Note: This is not an error, but you may want to consider:
605    ///   • Using the same version for consistency
606    ///   • These resources will be installed to different files
607    ///   • Each will work independently
608    ///   • Consider aligning versions for 'community:agents/helper.md' across all resources
609    /// ```
610    #[must_use]
611    pub fn generate_redundancy_warning(&self, redundancies: &[Redundancy]) -> String {
612        if redundancies.is_empty() {
613            return String::new();
614        }
615
616        let mut message =
617            format!("\n{} Redundant dependencies detected\n\n", "Warning:".yellow().bold());
618
619        for redundancy in redundancies {
620            message.push_str(&format!("{redundancy}\n"));
621        }
622
623        message.push_str(&format!(
624            "\n{} This is not an error, but you may want to consider:\n",
625            "Note:".blue()
626        ));
627        message.push_str("  • Using the same version for consistency\n");
628        message.push_str("  • These resources will be installed to different files\n");
629        message.push_str("  • Each will work independently\n");
630
631        // Add specific suggestions based on redundancy patterns
632        for redundancy in redundancies {
633            let has_latest = redundancy.usages.iter().any(|u| u.version.is_none());
634            let has_specific = redundancy.usages.iter().any(|u| u.version.is_some());
635
636            if has_latest && has_specific {
637                message.push_str(&format!(
638                    "  • Consider aligning versions for '{}' across all resources\n",
639                    redundancy.source_file
640                ));
641            }
642        }
643
644        message
645    }
646
647    /// Placeholder for future transitive redundancy detection.
648    ///
649    /// This method is reserved for future implementation when AGPM supports
650    /// dependencies-of-dependencies (transitive dependencies). Currently returns
651    /// an empty vector as transitive analysis is not yet implemented.
652    ///
653    /// # Planned Functionality
654    ///
655    /// When implemented, this method will:
656    /// 1. **Build Dependency Tree**: Map entire transitive dependency graph
657    /// 2. **Detect Deep Redundancy**: Find redundant patterns across dependency levels
658    /// 3. **Analyze Impact**: Calculate storage and maintenance implications
659    /// 4. **Suggest Optimizations**: Recommend dependency tree restructuring
660    ///
661    /// # Example Future Analysis
662    ///
663    /// ```text
664    /// Direct:     app-agent → community:agents/helper.md v1.0.0
665    /// Transitive: app-agent → tool-lib → community:agents/helper.md v2.0.0
666    ///
667    /// Result: Transitive redundancy detected - app-agent indirectly depends
668    ///         on two versions of the same resource.
669    /// ```
670    ///
671    /// # Implementation Challenges
672    ///
673    /// - **Circular Dependencies**: Detection and handling of cycles
674    /// - **Version Compatibility**: Analyzing semantic version compatibility
675    /// - **Performance**: Efficient analysis of large dependency trees
676    /// - **Cache Management**: Handling cached vs. fresh transitive data
677    ///
678    /// # Returns
679    ///
680    /// Currently returns an empty vector. Future implementation will return
681    /// detected transitive redundancies.
682    #[must_use]
683    pub const fn check_transitive_redundancies(&self) -> Vec<Redundancy> {
684        // TODO: When we add support for dependencies having their own dependencies,
685        // we'll need to check for redundancies across the entire dependency tree
686        Vec::new()
687    }
688
689    /// Generates actionable consolidation strategies for a specific redundancy.
690    ///
691    /// This method analyzes a detected redundancy pattern and provides specific,
692    /// actionable suggestions for resolving or managing the redundancy. The
693    /// suggestions are tailored to the specific pattern of version usage.
694    ///
695    /// # Strategy Categories
696    ///
697    /// ## Version Alignment
698    /// For redundancies with multiple specific versions:
699    /// - Suggest adopting a single version across all resources
700    /// - Recommend the most recent or most commonly used version
701    ///
702    /// ## Constraint Standardization
703    /// For mixed latest/specific version patterns:
704    /// - Suggest using specific versions for reproducibility
705    /// - Explain benefits of version pinning
706    ///
707    /// ## Impact Assessment
708    /// For all redundancies:
709    /// - Clarify that resources will be installed independently
710    /// - Explain that each resource will function correctly
711    /// - List all affected resource names
712    ///
713    /// # Suggestion Algorithm
714    ///
715    /// 1. **Analyze Version Pattern**: Identify specific vs. latest usage
716    /// 2. **Generate Alignment Suggestions**: Recommend version standardization
717    /// 3. **Provide Context**: Explain implications and benefits
718    /// 4. **List Affected Resources**: Show impact scope
719    ///
720    /// # Parameters
721    ///
722    /// - `redundancy`: The redundancy pattern to analyze
723    ///
724    /// # Returns
725    ///
726    /// A vector of suggestion strings, ordered by priority:
727    /// 1. Primary suggestions (version alignment)
728    /// 2. Best practice recommendations (reproducibility)
729    /// 3. Impact clarification (what will actually happen)
730    ///
731    /// # Example Output
732    ///
733    /// For a redundancy with mixed version constraints:
734    /// ```text
735    /// "Consider using version v2.0.0 for all resources using 'community:agents/helper.md'"
736    /// "Consider using specific versions for all resources for reproducibility"
737    /// "Note: Each resource (app-helper, tool-helper) will be installed independently"
738    /// ```
739    ///
740    /// # Use Cases
741    ///
742    /// - **CLI Tools**: Generate help text for redundancy warnings
743    /// - **IDE Extensions**: Provide quick-fix suggestions
744    /// - **Automated Tools**: Implement dependency optimization utilities
745    /// - **Documentation**: Generate project-specific optimization guides
746    #[must_use]
747    pub fn suggest_consolidation(&self, redundancy: &Redundancy) -> Vec<String> {
748        let mut suggestions = Vec::new();
749
750        // Collect all versions being used
751        let versions: Vec<_> =
752            redundancy.usages.iter().filter_map(|u| u.version.as_ref()).collect();
753
754        if !versions.is_empty() {
755            // Suggest using the same version for consistency
756            if let Some(version) = versions.first() {
757                suggestions.push(format!(
758                    "Consider using version {} for all resources using '{}'",
759                    version, redundancy.source_file
760                ));
761            }
762        }
763
764        // If mixing latest and specific versions
765        let has_latest = redundancy.usages.iter().any(|u| u.version.is_none());
766        let has_specific = redundancy.usages.iter().any(|u| u.version.is_some());
767
768        if has_latest && has_specific {
769            suggestions.push(
770                "Consider using specific versions for all resources for reproducibility"
771                    .to_string(),
772            );
773        }
774
775        // Explain that this isn't breaking
776        suggestions.push(format!(
777            "Note: Each resource ({}) will be installed independently",
778            redundancy
779                .usages
780                .iter()
781                .map(|u| &u.resource_name)
782                .cloned()
783                .collect::<Vec<_>>()
784                .join(", ")
785        ));
786
787        suggestions
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794    use crate::manifest::DetailedDependency;
795
796    /// Tests basic redundancy detection with different versions of the same resource.
797    ///
798    /// This test verifies that the detector correctly identifies when multiple
799    /// resources reference the same source file with different version constraints.
800    #[test]
801    fn test_detect_simple_redundancy() {
802        let mut detector = RedundancyDetector::new();
803
804        // Add resources using different versions of the same source file
805        detector.add_usage(
806            "app-agent".to_string(),
807            &ResourceDependency::Detailed(Box::new(DetailedDependency {
808                source: Some("community".to_string()),
809                path: "agents/shared.md".to_string(),
810                version: Some("v1.0.0".to_string()),
811                branch: None,
812                rev: None,
813                command: None,
814                args: None,
815                target: None,
816                filename: None,
817                dependencies: None,
818                tool: "claude-code".to_string(),
819            })),
820        );
821
822        detector.add_usage(
823            "tool-agent".to_string(),
824            &ResourceDependency::Detailed(Box::new(DetailedDependency {
825                source: Some("community".to_string()),
826                path: "agents/shared.md".to_string(),
827                version: Some("v2.0.0".to_string()),
828                branch: None,
829                rev: None,
830                command: None,
831                args: None,
832                target: None,
833                filename: None,
834                dependencies: None,
835                tool: "claude-code".to_string(),
836            })),
837        );
838
839        let redundancies = detector.detect_redundancies();
840        assert_eq!(redundancies.len(), 1);
841
842        let redundancy = &redundancies[0];
843        assert_eq!(redundancy.source_file, "community:agents/shared.md");
844        assert_eq!(redundancy.usages.len(), 2);
845    }
846
847    /// Tests that resources using the same version are not flagged as redundant.
848    ///
849    /// This test ensures the detector doesn't generate false positives when
850    /// multiple resources legitimately use the same source file and version.
851    #[test]
852    fn test_no_redundancy_same_version() {
853        let mut detector = RedundancyDetector::new();
854
855        // Add resources using the same version - not considered redundant
856        detector.add_usage(
857            "agent1".to_string(),
858            &ResourceDependency::Detailed(Box::new(DetailedDependency {
859                source: Some("community".to_string()),
860                path: "agents/shared.md".to_string(),
861                version: Some("v1.0.0".to_string()),
862                branch: None,
863                rev: None,
864                command: None,
865                args: None,
866                target: None,
867                filename: None,
868                dependencies: None,
869                tool: "claude-code".to_string(),
870            })),
871        );
872
873        detector.add_usage(
874            "agent2".to_string(),
875            &ResourceDependency::Detailed(Box::new(DetailedDependency {
876                source: Some("community".to_string()),
877                path: "agents/shared.md".to_string(),
878                version: Some("v1.0.0".to_string()),
879                branch: None,
880                rev: None,
881                command: None,
882                args: None,
883                target: None,
884                filename: None,
885                dependencies: None,
886                tool: "claude-code".to_string(),
887            })),
888        );
889
890        let redundancies = detector.detect_redundancies();
891        assert_eq!(redundancies.len(), 0);
892    }
893
894    /// Tests detection of mixed latest/specific version redundancy patterns.
895    ///
896    /// This test verifies that the detector identifies redundancy when some
897    /// resources use latest (no version) while others specify explicit versions.
898    #[test]
899    fn test_redundancy_latest_vs_specific() {
900        let mut detector = RedundancyDetector::new();
901
902        // One wants latest, another wants specific version - this is redundant
903        detector.add_usage(
904            "agent1".to_string(),
905            &ResourceDependency::Detailed(Box::new(DetailedDependency {
906                source: Some("community".to_string()),
907                path: "agents/shared.md".to_string(),
908                version: None, // latest
909                branch: None,
910                rev: None,
911                command: None,
912                args: None,
913                target: None,
914                filename: None,
915                dependencies: None,
916                tool: "claude-code".to_string(),
917            })),
918        );
919
920        detector.add_usage(
921            "agent2".to_string(),
922            &ResourceDependency::Detailed(Box::new(DetailedDependency {
923                source: Some("community".to_string()),
924                path: "agents/shared.md".to_string(),
925                version: Some("v1.0.0".to_string()),
926                branch: None,
927                rev: None,
928                command: None,
929                args: None,
930                target: None,
931                filename: None,
932                dependencies: None,
933                tool: "claude-code".to_string(),
934            })),
935        );
936
937        let redundancies = detector.detect_redundancies();
938        assert_eq!(redundancies.len(), 1);
939    }
940
941    /// Tests that local dependencies are properly filtered out of redundancy analysis.
942    ///
943    /// This test ensures that local file dependencies don't participate in
944    /// redundancy detection since they don't have the source/version complexity.
945    #[test]
946    fn test_local_dependencies_ignored() {
947        let mut detector = RedundancyDetector::new();
948
949        // Local dependencies are not tracked for redundancy
950        detector.add_usage(
951            "local1".to_string(),
952            &ResourceDependency::Simple("../agents/agent1.md".to_string()),
953        );
954
955        detector.add_usage(
956            "local2".to_string(),
957            &ResourceDependency::Simple("../agents/agent2.md".to_string()),
958        );
959
960        let redundancies = detector.detect_redundancies();
961        assert_eq!(redundancies.len(), 0);
962    }
963
964    /// Tests the generation of comprehensive warning messages for redundancies.
965    ///
966    /// This test verifies that the warning message generator produces appropriate
967    /// content including resource names, versions, and helpful guidance.
968    #[test]
969    fn test_generate_redundancy_warning() {
970        let mut detector = RedundancyDetector::new();
971
972        detector.add_usage(
973            "app".to_string(),
974            &ResourceDependency::Detailed(Box::new(DetailedDependency {
975                source: Some("community".to_string()),
976                path: "agents/shared.md".to_string(),
977                version: Some("v1.0.0".to_string()),
978                branch: None,
979                rev: None,
980                command: None,
981                args: None,
982                target: None,
983                filename: None,
984                dependencies: None,
985                tool: "claude-code".to_string(),
986            })),
987        );
988
989        detector.add_usage(
990            "tool".to_string(),
991            &ResourceDependency::Detailed(Box::new(DetailedDependency {
992                source: Some("community".to_string()),
993                path: "agents/shared.md".to_string(),
994                version: Some("v2.0.0".to_string()),
995                branch: None,
996                rev: None,
997                command: None,
998                args: None,
999                target: None,
1000                filename: None,
1001                dependencies: None,
1002                tool: "claude-code".to_string(),
1003            })),
1004        );
1005
1006        let redundancies = detector.detect_redundancies();
1007        let warning = detector.generate_redundancy_warning(&redundancies);
1008
1009        assert!(warning.contains("Redundant dependencies detected"));
1010        assert!(warning.contains("app"));
1011        assert!(warning.contains("tool"));
1012        assert!(warning.contains("not an error"));
1013    }
1014
1015    /// Tests the generation of consolidation suggestions for redundancy patterns.
1016    ///
1017    /// This test verifies that the suggestion generator produces actionable
1018    /// recommendations for resolving detected redundancy patterns.
1019    #[test]
1020    fn test_suggest_consolidation() {
1021        let mut detector = RedundancyDetector::new();
1022
1023        detector.add_usage(
1024            "app".to_string(),
1025            &ResourceDependency::Detailed(Box::new(DetailedDependency {
1026                source: Some("community".to_string()),
1027                path: "agents/shared.md".to_string(),
1028                version: None, // latest
1029                branch: None,
1030                rev: None,
1031                command: None,
1032                args: None,
1033                target: None,
1034                filename: None,
1035                dependencies: None,
1036                tool: "claude-code".to_string(),
1037            })),
1038        );
1039
1040        detector.add_usage(
1041            "tool".to_string(),
1042            &ResourceDependency::Detailed(Box::new(DetailedDependency {
1043                source: Some("community".to_string()),
1044                path: "agents/shared.md".to_string(),
1045                version: Some("v2.0.0".to_string()),
1046                branch: None,
1047                rev: None,
1048                command: None,
1049                args: None,
1050                target: None,
1051                filename: None,
1052                dependencies: None,
1053                tool: "claude-code".to_string(),
1054            })),
1055        );
1056
1057        let redundancies = detector.detect_redundancies();
1058        let suggestions = detector.suggest_consolidation(&redundancies[0]);
1059
1060        assert!(!suggestions.is_empty());
1061        assert!(suggestions.iter().any(|s| s.contains("v2.0.0")));
1062        assert!(suggestions.iter().any(|s| s.contains("independently")));
1063    }
1064}