ccpm/resolver/
redundancy.rs

1//! Redundancy detection and analysis for CCPM 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/ccpm-official.git"
31//! mirror = "https://github.com/org/ccpm-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 ccpm::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 ccpm::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 ccpm::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 ccpm::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 ccpm::resolver::redundancy::RedundancyDetector;
315    /// use ccpm::manifest::{ResourceDependency, DetailedDependency};
316    ///
317    /// let mut detector = RedundancyDetector::new();
318    ///
319    /// // This will be recorded
320    /// let remote_dep = ResourceDependency::Detailed(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    /// });
331    /// detector.add_usage("my-helper".to_string(), &remote_dep);
332    ///
333    /// // This will be ignored (local dependency)
334    /// let local_dep = ResourceDependency::Simple("../local/helper.md".to_string());
335    /// detector.add_usage("local-helper".to_string(), &local_dep);
336    /// ```
337    pub fn add_usage(&mut self, resource_name: String, dep: &ResourceDependency) {
338        // Only track remote dependencies (local ones don't have redundancy issues)
339        if dep.is_local() {
340            return;
341        }
342
343        if let Some(source) = dep.get_source() {
344            let source_file = format!("{}:{}", source, dep.get_path());
345
346            let usage = ResourceUsage {
347                resource_name,
348                source_file: source_file.clone(),
349                version: dep.get_version().map(std::string::ToString::to_string),
350            };
351
352            self.usages.entry(source_file).or_default().push(usage);
353        }
354    }
355
356    /// Analyzes all dependencies from a manifest for redundancy patterns.
357    ///
358    /// This is a convenience method that processes all dependencies from
359    /// a manifest file in a single operation. It's equivalent to calling
360    /// [`add_usage()`] for each dependency individually.
361    ///
362    /// # Processing Scope
363    ///
364    /// The method analyzes:
365    /// - **Agent Dependencies**: From `[agents]` section
366    /// - **Snippet Dependencies**: From `[snippets]` section
367    /// - **Remote Dependencies**: Only those with source specifications
368    ///
369    /// Local dependencies are automatically filtered out during analysis.
370    ///
371    /// # Usage Pattern
372    ///
373    /// This method is typically used in the main resolution workflow:
374    /// ```rust,no_run
375    /// use ccpm::resolver::redundancy::RedundancyDetector;
376    /// use ccpm::manifest::Manifest;
377    /// use std::path::Path;
378    ///
379    /// # fn example() -> anyhow::Result<()> {
380    /// let manifest = Manifest::load(Path::new("ccpm.toml"))?;
381    /// let mut detector = RedundancyDetector::new();
382    /// detector.analyze_manifest(&manifest);
383    ///
384    /// let redundancies = detector.detect_redundancies();
385    /// let warning = detector.generate_redundancy_warning(&redundancies);
386    /// if !warning.is_empty() {
387    ///     eprintln!("{}", warning);
388    /// }
389    /// # Ok(())
390    /// # }
391    /// ```
392    ///
393    /// # Performance
394    ///
395    /// - **Time Complexity**: O(n) where n = total dependencies
396    /// - **Space Complexity**: O(r) where r = remote dependencies
397    /// - **Memory Usage**: Linear with number of remote dependencies
398    ///
399    /// [`add_usage()`]: RedundancyDetector::add_usage
400    pub fn analyze_manifest(&mut self, manifest: &Manifest) {
401        for (name, dep) in manifest.all_dependencies() {
402            self.add_usage(name.to_string(), dep);
403        }
404    }
405
406    /// Detects redundancy patterns in the collected resource usages.
407    ///
408    /// This method analyzes all collected resource usages and identifies
409    /// patterns where multiple resources use the same source file with
410    /// different version constraints.
411    ///
412    /// # Detection Algorithm
413    ///
414    /// For each source file in the usage map:
415    /// 1. **Skip Single Usage**: Files used by only one resource are not redundant
416    /// 2. **Version Analysis**: Collect all unique version constraints for the file
417    /// 3. **Redundancy Check**: If multiple different versions exist, mark as redundant
418    ///
419    /// # Redundancy Criteria
420    ///
421    /// A source file is considered redundant when:
422    /// - **Multiple Resources**: More than one resource uses the file
423    /// - **Different Versions**: Resources specify different version constraints
424    ///
425    /// # Non-Redundant Cases
426    ///
427    /// These cases are NOT considered redundant:
428    /// - Single resource using a source file
429    /// - Multiple resources using identical version constraints
430    /// - Multiple resources all using "latest" (no version specified)
431    ///
432    /// # Algorithm Complexity
433    ///
434    /// - **Time**: O(n + k·m) where:
435    ///   - n = total resource usages
436    ///   - k = unique source files
437    ///   - m = average usages per file
438    /// - **Space**: O(r) where r = detected redundancies
439    ///
440    /// # Returns
441    ///
442    /// A vector of [`Redundancy`] objects, each representing a source file
443    /// with redundant usage patterns. The vector is empty if no redundancies
444    /// are detected.
445    ///
446    /// # Example Output
447    ///
448    /// For a manifest with redundant dependencies, this method might return:
449    /// ```text
450    /// [
451    ///     Redundancy {
452    ///         source_file: "community:agents/helper.md",
453    ///         usages: [
454    ///             ResourceUsage { resource_name: "app-helper", version: Some("v1.0.0") },
455    ///             ResourceUsage { resource_name: "tool-helper", version: Some("v2.0.0") },
456    ///         ]
457    ///     }
458    /// ]
459    /// ```
460    ///
461    /// [`Redundancy`]: Redundancy
462    #[must_use]
463    pub fn detect_redundancies(&self) -> Vec<Redundancy> {
464        let mut redundancies = Vec::new();
465
466        for (source_file, uses) in &self.usages {
467            // Skip if only one resource uses this source file
468            if uses.len() <= 1 {
469                continue;
470            }
471
472            // Collect unique versions
473            let versions: HashSet<Option<String>> =
474                uses.iter().map(|u| u.version.clone()).collect();
475
476            // If there are different versions, it's a redundancy worth noting
477            if versions.len() > 1 {
478                redundancies.push(Redundancy {
479                    source_file: source_file.clone(),
480                    usages: uses.clone(),
481                });
482            }
483        }
484
485        redundancies
486    }
487
488    /// Determines if a redundancy could be consolidated to use a single version.
489    ///
490    /// This method analyzes a detected redundancy to determine if all resources
491    /// using the source file could reasonably be updated to use the same version.
492    /// This is a heuristic for suggesting consolidation opportunities.
493    ///
494    /// # Consolidation Logic
495    ///
496    /// A redundancy can be consolidated if:
497    /// - All resources use the same version constraint (already consolidated)
498    /// - All resources use "latest" (no specific versions)
499    ///
500    /// A redundancy cannot be easily consolidated if:
501    /// - Resources use different specific versions (may have compatibility reasons)
502    /// - Mixed latest and specific versions (may indicate intentional pinning)
503    ///
504    /// # Use Cases
505    ///
506    /// This method helps identify:
507    /// - **Easy Wins**: Redundancies that could be quickly resolved
508    /// - **Complex Cases**: Redundancies that may require careful consideration
509    /// - **Intentional Patterns**: Cases where redundancy might be deliberate
510    ///
511    /// # Parameters
512    ///
513    /// - `redundancy`: The redundancy pattern to analyze
514    ///
515    /// # Returns
516    ///
517    /// - `true`: All usages could likely be consolidated to a single version
518    /// - `false`: Consolidation would require careful analysis of compatibility
519    ///
520    /// # Example
521    ///
522    /// ```rust,no_run
523    /// use ccpm::resolver::redundancy::{RedundancyDetector, Redundancy, ResourceUsage};
524    ///
525    /// let detector = RedundancyDetector::new();
526    ///
527    /// // Easy to consolidate (all use latest)
528    /// let easy_redundancy = Redundancy {
529    ///     source_file: "community:agents/helper.md".to_string(),
530    ///     usages: vec![
531    ///         ResourceUsage { resource_name: "helper1".to_string(), source_file: "community:agents/helper.md".to_string(), version: None },
532    ///         ResourceUsage { resource_name: "helper2".to_string(), source_file: "community:agents/helper.md".to_string(), version: None },
533    ///     ]
534    /// };
535    /// assert!(detector.can_consolidate(&easy_redundancy));
536    ///
537    /// // Hard to consolidate (different versions)
538    /// let hard_redundancy = Redundancy {
539    ///     source_file: "community:agents/helper.md".to_string(),
540    ///     usages: vec![
541    ///         ResourceUsage { resource_name: "helper1".to_string(), source_file: "community:agents/helper.md".to_string(), version: Some("v1.0.0".to_string()) },
542    ///         ResourceUsage { resource_name: "helper2".to_string(), source_file: "community:agents/helper.md".to_string(), version: Some("v2.0.0".to_string()) },
543    ///     ]
544    /// };
545    /// assert!(!detector.can_consolidate(&hard_redundancy));
546    /// ```
547    #[must_use]
548    pub fn can_consolidate(&self, redundancy: &Redundancy) -> bool {
549        // If all usages want the same version or latest, they could be consolidated
550        let versions: HashSet<_> = redundancy.usages.iter().map(|u| &u.version).collect();
551        versions.len() == 1
552    }
553
554    /// Generates a comprehensive warning message for detected redundancies.
555    ///
556    /// This method creates a user-friendly warning message that explains detected
557    /// redundancies and provides actionable suggestions for optimization. The
558    /// message is designed to be informative rather than alarming, emphasizing
559    /// that redundancy is not an error.
560    ///
561    /// # Message Structure
562    ///
563    /// The generated warning includes:
564    /// 1. **Header**: Clear indication this is a warning, not an error
565    /// 2. **Redundancy List**: Each detected redundancy with details
566    /// 3. **General Guidance**: Explanation of implications and options
567    /// 4. **Specific Suggestions**: Targeted advice based on detected patterns
568    ///
569    /// # Message Tone
570    ///
571    /// The warning message maintains a helpful, non-blocking tone:
572    /// - Emphasizes that installation will proceed normally
573    /// - Explains that redundancy may be intentional
574    /// - Provides optimization suggestions without mandating changes
575    /// - Uses clear, jargon-free language
576    ///
577    /// # Color Coding
578    ///
579    /// The message uses terminal colors for better readability:
580    /// - **Yellow**: Warning indicators and attention markers
581    /// - **Blue**: Informational notes and suggestions
582    /// - **Default**: Main content and resource names
583    ///
584    /// # Parameters
585    ///
586    /// - `redundancies`: List of detected redundancy patterns
587    ///
588    /// # Returns
589    ///
590    /// - **Non-empty**: Formatted warning message if redundancies exist
591    /// - **Empty string**: If no redundancies provided
592    ///
593    /// # Example Output
594    ///
595    /// ```text
596    /// Warning: Redundant dependencies detected
597    ///
598    /// ⚠ Multiple versions of 'community:agents/helper.md' will be installed:
599    ///   - 'app-helper' uses version v1.0.0
600    ///   - 'tool-helper' uses version latest
601    ///
602    /// Note: This is not an error, but you may want to consider:
603    ///   • Using the same version for consistency
604    ///   • These resources will be installed to different files
605    ///   • Each will work independently
606    ///   • Consider aligning versions for 'community:agents/helper.md' across all resources
607    /// ```
608    #[must_use]
609    pub fn generate_redundancy_warning(&self, redundancies: &[Redundancy]) -> String {
610        if redundancies.is_empty() {
611            return String::new();
612        }
613
614        let mut message = format!(
615            "\n{} Redundant dependencies detected\n\n",
616            "Warning:".yellow().bold()
617        );
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 CCPM 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 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<_> = redundancy
752            .usages
753            .iter()
754            .filter_map(|u| u.version.as_ref())
755            .collect();
756
757        if !versions.is_empty() {
758            // Suggest using the same version for consistency
759            if let Some(version) = versions.first() {
760                suggestions.push(format!(
761                    "Consider using version {} for all resources using '{}'",
762                    version, redundancy.source_file
763                ));
764            }
765        }
766
767        // If mixing latest and specific versions
768        let has_latest = redundancy.usages.iter().any(|u| u.version.is_none());
769        let has_specific = redundancy.usages.iter().any(|u| u.version.is_some());
770
771        if has_latest && has_specific {
772            suggestions.push(
773                "Consider using specific versions for all resources for reproducibility"
774                    .to_string(),
775            );
776        }
777
778        // Explain that this isn't breaking
779        suggestions.push(format!(
780            "Note: Each resource ({}) will be installed independently",
781            redundancy
782                .usages
783                .iter()
784                .map(|u| &u.resource_name)
785                .cloned()
786                .collect::<Vec<_>>()
787                .join(", ")
788        ));
789
790        suggestions
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use crate::manifest::DetailedDependency;
798
799    /// Tests basic redundancy detection with different versions of the same resource.
800    ///
801    /// This test verifies that the detector correctly identifies when multiple
802    /// resources reference the same source file with different version constraints.
803    #[test]
804    fn test_detect_simple_redundancy() {
805        let mut detector = RedundancyDetector::new();
806
807        // Add resources using different versions of the same source file
808        detector.add_usage(
809            "app-agent".to_string(),
810            &ResourceDependency::Detailed(DetailedDependency {
811                source: Some("community".to_string()),
812                path: "agents/shared.md".to_string(),
813                version: Some("v1.0.0".to_string()),
814                branch: None,
815                rev: None,
816                command: None,
817                args: None,
818                target: None,
819                filename: None,
820            }),
821        );
822
823        detector.add_usage(
824            "tool-agent".to_string(),
825            &ResourceDependency::Detailed(DetailedDependency {
826                source: Some("community".to_string()),
827                path: "agents/shared.md".to_string(),
828                version: Some("v2.0.0".to_string()),
829                branch: None,
830                rev: None,
831                command: None,
832                args: None,
833                target: None,
834                filename: None,
835            }),
836        );
837
838        let redundancies = detector.detect_redundancies();
839        assert_eq!(redundancies.len(), 1);
840
841        let redundancy = &redundancies[0];
842        assert_eq!(redundancy.source_file, "community:agents/shared.md");
843        assert_eq!(redundancy.usages.len(), 2);
844    }
845
846    /// Tests that resources using the same version are not flagged as redundant.
847    ///
848    /// This test ensures the detector doesn't generate false positives when
849    /// multiple resources legitimately use the same source file and version.
850    #[test]
851    fn test_no_redundancy_same_version() {
852        let mut detector = RedundancyDetector::new();
853
854        // Add resources using the same version - not considered redundant
855        detector.add_usage(
856            "agent1".to_string(),
857            &ResourceDependency::Detailed(DetailedDependency {
858                source: Some("community".to_string()),
859                path: "agents/shared.md".to_string(),
860                version: Some("v1.0.0".to_string()),
861                branch: None,
862                rev: None,
863                command: None,
864                args: None,
865                target: None,
866                filename: None,
867            }),
868        );
869
870        detector.add_usage(
871            "agent2".to_string(),
872            &ResourceDependency::Detailed(DetailedDependency {
873                source: Some("community".to_string()),
874                path: "agents/shared.md".to_string(),
875                version: Some("v1.0.0".to_string()),
876                branch: None,
877                rev: None,
878                command: None,
879                args: None,
880                target: None,
881                filename: None,
882            }),
883        );
884
885        let redundancies = detector.detect_redundancies();
886        assert_eq!(redundancies.len(), 0);
887    }
888
889    /// Tests detection of mixed latest/specific version redundancy patterns.
890    ///
891    /// This test verifies that the detector identifies redundancy when some
892    /// resources use latest (no version) while others specify explicit versions.
893    #[test]
894    fn test_redundancy_latest_vs_specific() {
895        let mut detector = RedundancyDetector::new();
896
897        // One wants latest, another wants specific version - this is redundant
898        detector.add_usage(
899            "agent1".to_string(),
900            &ResourceDependency::Detailed(DetailedDependency {
901                source: Some("community".to_string()),
902                path: "agents/shared.md".to_string(),
903                version: None, // latest
904                branch: None,
905                rev: None,
906                command: None,
907                args: None,
908                target: None,
909                filename: None,
910            }),
911        );
912
913        detector.add_usage(
914            "agent2".to_string(),
915            &ResourceDependency::Detailed(DetailedDependency {
916                source: Some("community".to_string()),
917                path: "agents/shared.md".to_string(),
918                version: Some("v1.0.0".to_string()),
919                branch: None,
920                rev: None,
921                command: None,
922                args: None,
923                target: None,
924                filename: None,
925            }),
926        );
927
928        let redundancies = detector.detect_redundancies();
929        assert_eq!(redundancies.len(), 1);
930    }
931
932    /// Tests that local dependencies are properly filtered out of redundancy analysis.
933    ///
934    /// This test ensures that local file dependencies don't participate in
935    /// redundancy detection since they don't have the source/version complexity.
936    #[test]
937    fn test_local_dependencies_ignored() {
938        let mut detector = RedundancyDetector::new();
939
940        // Local dependencies are not tracked for redundancy
941        detector.add_usage(
942            "local1".to_string(),
943            &ResourceDependency::Simple("../agents/agent1.md".to_string()),
944        );
945
946        detector.add_usage(
947            "local2".to_string(),
948            &ResourceDependency::Simple("../agents/agent2.md".to_string()),
949        );
950
951        let redundancies = detector.detect_redundancies();
952        assert_eq!(redundancies.len(), 0);
953    }
954
955    /// Tests the generation of comprehensive warning messages for redundancies.
956    ///
957    /// This test verifies that the warning message generator produces appropriate
958    /// content including resource names, versions, and helpful guidance.
959    #[test]
960    fn test_generate_redundancy_warning() {
961        let mut detector = RedundancyDetector::new();
962
963        detector.add_usage(
964            "app".to_string(),
965            &ResourceDependency::Detailed(DetailedDependency {
966                source: Some("community".to_string()),
967                path: "agents/shared.md".to_string(),
968                version: Some("v1.0.0".to_string()),
969                branch: None,
970                rev: None,
971                command: None,
972                args: None,
973                target: None,
974                filename: None,
975            }),
976        );
977
978        detector.add_usage(
979            "tool".to_string(),
980            &ResourceDependency::Detailed(DetailedDependency {
981                source: Some("community".to_string()),
982                path: "agents/shared.md".to_string(),
983                version: Some("v2.0.0".to_string()),
984                branch: None,
985                rev: None,
986                command: None,
987                args: None,
988                target: None,
989                filename: None,
990            }),
991        );
992
993        let redundancies = detector.detect_redundancies();
994        let warning = detector.generate_redundancy_warning(&redundancies);
995
996        assert!(warning.contains("Redundant dependencies detected"));
997        assert!(warning.contains("app"));
998        assert!(warning.contains("tool"));
999        assert!(warning.contains("not an error"));
1000    }
1001
1002    /// Tests the generation of consolidation suggestions for redundancy patterns.
1003    ///
1004    /// This test verifies that the suggestion generator produces actionable
1005    /// recommendations for resolving detected redundancy patterns.
1006    #[test]
1007    fn test_suggest_consolidation() {
1008        let mut detector = RedundancyDetector::new();
1009
1010        detector.add_usage(
1011            "app".to_string(),
1012            &ResourceDependency::Detailed(DetailedDependency {
1013                source: Some("community".to_string()),
1014                path: "agents/shared.md".to_string(),
1015                version: None, // latest
1016                branch: None,
1017                rev: None,
1018                command: None,
1019                args: None,
1020                target: None,
1021                filename: None,
1022            }),
1023        );
1024
1025        detector.add_usage(
1026            "tool".to_string(),
1027            &ResourceDependency::Detailed(DetailedDependency {
1028                source: Some("community".to_string()),
1029                path: "agents/shared.md".to_string(),
1030                version: Some("v2.0.0".to_string()),
1031                branch: None,
1032                rev: None,
1033                command: None,
1034                args: None,
1035                target: None,
1036                filename: None,
1037            }),
1038        );
1039
1040        let redundancies = detector.detect_redundancies();
1041        let suggestions = detector.suggest_consolidation(&redundancies[0]);
1042
1043        assert!(!suggestions.is_empty());
1044        assert!(suggestions.iter().any(|s| s.contains("v2.0.0")));
1045        assert!(suggestions.iter().any(|s| s.contains("independently")));
1046    }
1047}