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}