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}