cargo_coupling/
balance.rs

1//! Balance score calculation and refactoring recommendations
2//!
3//! Implements the balance equation from "Balancing Coupling in Software Design":
4//! BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY
5//!
6//! Key principle: The goal is NOT to eliminate coupling, but to balance it appropriately.
7//! - Strong coupling + close distance = Good (cohesion)
8//! - Weak coupling + far distance = Good (loose coupling)
9//! - Strong coupling + far distance = Bad (global complexity)
10//! - High volatility + strong coupling = Bad (cascading changes)
11
12use std::collections::HashMap;
13
14use crate::metrics::{CouplingMetrics, Distance, IntegrationStrength, ProjectMetrics, Volatility};
15
16/// Issue severity levels
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Severity {
19    /// Minor issue, consider addressing
20    Low,
21    /// Should be addressed in regular maintenance
22    Medium,
23    /// Needs attention soon, potential source of bugs
24    High,
25    /// Must be fixed, actively causing problems
26    Critical,
27}
28
29impl std::fmt::Display for Severity {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Severity::Low => write!(f, "Low"),
33            Severity::Medium => write!(f, "Medium"),
34            Severity::High => write!(f, "High"),
35            Severity::Critical => write!(f, "Critical"),
36        }
37    }
38}
39
40/// Types of coupling problems
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum IssueType {
43    /// Strong coupling spanning a long distance
44    GlobalComplexity,
45    /// Strong coupling to a frequently changing component
46    CascadingChangeRisk,
47    /// Intrusive coupling across boundaries (field/internals access)
48    InappropriateIntimacy,
49    /// A module with too many dependencies
50    HighEfferentCoupling,
51    /// A module that too many others depend on
52    HighAfferentCoupling,
53    /// Weak coupling where stronger might be appropriate
54    UnnecessaryAbstraction,
55    /// Circular dependency detected
56    CircularDependency,
57
58    // === APOSD-inspired issues (A Philosophy of Software Design) ===
59    /// Module with interface complexity close to implementation complexity
60    ShallowModule,
61    /// Method that only delegates to another method without adding value
62    PassThroughMethod,
63    /// Module requiring too much knowledge to understand/modify
64    HighCognitiveLoad,
65}
66
67impl std::fmt::Display for IssueType {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            IssueType::GlobalComplexity => write!(f, "Global Complexity"),
71            IssueType::CascadingChangeRisk => write!(f, "Cascading Change Risk"),
72            IssueType::InappropriateIntimacy => write!(f, "Inappropriate Intimacy"),
73            IssueType::HighEfferentCoupling => write!(f, "High Efferent Coupling"),
74            IssueType::HighAfferentCoupling => write!(f, "High Afferent Coupling"),
75            IssueType::UnnecessaryAbstraction => write!(f, "Unnecessary Abstraction"),
76            IssueType::CircularDependency => write!(f, "Circular Dependency"),
77            // APOSD-inspired
78            IssueType::ShallowModule => write!(f, "Shallow Module"),
79            IssueType::PassThroughMethod => write!(f, "Pass-Through Method"),
80            IssueType::HighCognitiveLoad => write!(f, "High Cognitive Load"),
81        }
82    }
83}
84
85impl IssueType {
86    /// Get a detailed description of what this issue type means
87    pub fn description(&self) -> &'static str {
88        match self {
89            IssueType::GlobalComplexity => {
90                "Strong coupling to distant components increases cognitive load and makes the system harder to understand and modify."
91            }
92            IssueType::CascadingChangeRisk => {
93                "Strongly coupling to volatile components means changes will cascade through the system, requiring updates in many places."
94            }
95            IssueType::InappropriateIntimacy => {
96                "Direct access to internal details (fields, private methods) across module boundaries violates encapsulation."
97            }
98            IssueType::HighEfferentCoupling => {
99                "A module depending on too many others is fragile and hard to test. Changes anywhere affect this module."
100            }
101            IssueType::HighAfferentCoupling => {
102                "A module that many others depend on is hard to change. Any modification risks breaking dependents."
103            }
104            IssueType::UnnecessaryAbstraction => {
105                "Using abstract interfaces for closely-related stable components may add complexity without benefit."
106            }
107            IssueType::CircularDependency => {
108                "Circular dependencies make it impossible to understand, test, or modify components in isolation."
109            }
110            // APOSD-inspired descriptions
111            IssueType::ShallowModule => {
112                "Interface complexity is close to implementation complexity. The module doesn't hide enough complexity behind a simple interface. (APOSD: Deep vs Shallow Modules)"
113            }
114            IssueType::PassThroughMethod => {
115                "Method only delegates to another method without adding significant functionality. Indicates unclear responsibility division. (APOSD: Pass-Through Methods)"
116            }
117            IssueType::HighCognitiveLoad => {
118                "Module requires too much knowledge to understand and modify. Too many public APIs, dependencies, or complex type signatures. (APOSD: Cognitive Load)"
119            }
120        }
121    }
122}
123
124/// A detected coupling issue with refactoring recommendation
125#[derive(Debug, Clone)]
126pub struct CouplingIssue {
127    /// Type of issue
128    pub issue_type: IssueType,
129    /// Severity of the issue
130    pub severity: Severity,
131    /// Source component
132    pub source: String,
133    /// Target component
134    pub target: String,
135    /// Specific description of this instance
136    pub description: String,
137    /// Concrete refactoring action to take
138    pub refactoring: RefactoringAction,
139    /// Balance score that triggered this issue
140    pub balance_score: f64,
141}
142
143/// Specific refactoring actions
144#[derive(Debug, Clone)]
145pub enum RefactoringAction {
146    /// Introduce a trait to abstract the coupling
147    IntroduceTrait {
148        suggested_name: String,
149        methods: Vec<String>,
150    },
151    /// Move the component closer (same module/crate)
152    MoveCloser { target_location: String },
153    /// Extract an interface/adapter
154    ExtractAdapter {
155        adapter_name: String,
156        purpose: String,
157    },
158    /// Split a large module
159    SplitModule { suggested_modules: Vec<String> },
160    /// Remove unnecessary abstraction
161    SimplifyAbstraction { direct_usage: String },
162    /// Break circular dependency
163    BreakCycle { suggested_direction: String },
164    /// Add stable interface
165    StabilizeInterface { interface_name: String },
166    /// General refactoring suggestion
167    General { action: String },
168}
169
170impl std::fmt::Display for RefactoringAction {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            RefactoringAction::IntroduceTrait {
174                suggested_name,
175                methods,
176            } => {
177                write!(
178                    f,
179                    "Introduce trait `{}` with methods: {}",
180                    suggested_name,
181                    methods.join(", ")
182                )
183            }
184            RefactoringAction::MoveCloser { target_location } => {
185                write!(f, "Move component to `{}`", target_location)
186            }
187            RefactoringAction::ExtractAdapter {
188                adapter_name,
189                purpose,
190            } => {
191                write!(f, "Extract adapter `{}` to {}", adapter_name, purpose)
192            }
193            RefactoringAction::SplitModule { suggested_modules } => {
194                write!(f, "Split into modules: {}", suggested_modules.join(", "))
195            }
196            RefactoringAction::SimplifyAbstraction { direct_usage } => {
197                write!(f, "Replace with direct usage: {}", direct_usage)
198            }
199            RefactoringAction::BreakCycle {
200                suggested_direction,
201            } => {
202                write!(f, "Break cycle by {}", suggested_direction)
203            }
204            RefactoringAction::StabilizeInterface { interface_name } => {
205                write!(f, "Add stable interface `{}`", interface_name)
206            }
207            RefactoringAction::General { action } => {
208                write!(f, "{}", action)
209            }
210        }
211    }
212}
213
214/// Balance score for a coupling relationship
215#[derive(Debug, Clone)]
216pub struct BalanceScore {
217    /// The coupling being scored
218    pub coupling: CouplingMetrics,
219    /// Overall balance score (0.0 - 1.0, higher = better balanced)
220    pub score: f64,
221    /// Whether strength and distance are aligned
222    pub alignment: f64,
223    /// Impact of volatility
224    pub volatility_impact: f64,
225    /// Interpretation of the score
226    pub interpretation: BalanceInterpretation,
227}
228
229/// How to interpret a balance score
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum BalanceInterpretation {
232    /// Well-balanced, no action needed
233    Balanced,
234    /// Acceptable but could be improved
235    Acceptable,
236    /// Should be reviewed
237    NeedsReview,
238    /// Should be refactored
239    NeedsRefactoring,
240    /// Critical issue, must fix
241    Critical,
242}
243
244impl std::fmt::Display for BalanceInterpretation {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self {
247            BalanceInterpretation::Balanced => write!(f, "Balanced"),
248            BalanceInterpretation::Acceptable => write!(f, "Acceptable"),
249            BalanceInterpretation::NeedsReview => write!(f, "Needs Review"),
250            BalanceInterpretation::NeedsRefactoring => write!(f, "Needs Refactoring"),
251            BalanceInterpretation::Critical => write!(f, "Critical"),
252        }
253    }
254}
255
256impl BalanceScore {
257    /// Calculate balance score for a coupling
258    ///
259    /// The formula implements: BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY
260    ///
261    /// Ideal patterns:
262    /// - Strong (1.0) + Close (0.0) → High alignment (cohesion)
263    /// - Weak (0.0) + Far (1.0) → High alignment (loose coupling)
264    ///
265    /// Problematic patterns:
266    /// - Strong (1.0) + Far (1.0) → Low alignment (global complexity)
267    /// - Any + High volatility → Reduced by volatility impact
268    pub fn calculate(coupling: &CouplingMetrics) -> Self {
269        let strength = coupling.strength_value();
270        let distance = coupling.distance_value();
271        let volatility = coupling.volatility_value();
272
273        // Alignment: how well strength and distance match the ideal patterns
274        // Ideal: (strong + close) OR (weak + far)
275        // XOR-like: difference between strength and distance
276        // If both high or both low = misaligned, if opposite = aligned
277        let alignment = 1.0 - (strength - (1.0 - distance)).abs();
278
279        // Volatility impact: high volatility with strong coupling is bad
280        // Only applies when there's actual coupling (strength > 0)
281        let volatility_penalty = volatility * strength;
282        let volatility_impact = 1.0 - volatility_penalty;
283
284        // Combined score: both alignment AND stability matter
285        // Using AND (multiplication) instead of OR (max) for stricter scoring
286        let score = alignment * volatility_impact;
287
288        // Determine interpretation based on score
289        let interpretation = match score {
290            s if s >= 0.8 => BalanceInterpretation::Balanced,
291            s if s >= 0.6 => BalanceInterpretation::Acceptable,
292            s if s >= 0.4 => BalanceInterpretation::NeedsReview,
293            s if s >= 0.2 => BalanceInterpretation::NeedsRefactoring,
294            _ => BalanceInterpretation::Critical,
295        };
296
297        Self {
298            coupling: coupling.clone(),
299            score,
300            alignment,
301            volatility_impact,
302            interpretation,
303        }
304    }
305
306    /// Check if this coupling is well-balanced (no action needed)
307    pub fn is_balanced(&self) -> bool {
308        matches!(
309            self.interpretation,
310            BalanceInterpretation::Balanced | BalanceInterpretation::Acceptable
311        )
312    }
313
314    /// Check if this coupling needs refactoring
315    pub fn needs_refactoring(&self) -> bool {
316        matches!(
317            self.interpretation,
318            BalanceInterpretation::NeedsRefactoring | BalanceInterpretation::Critical
319        )
320    }
321}
322
323/// Thresholds for identifying issues
324#[derive(Debug, Clone)]
325pub struct IssueThresholds {
326    /// Minimum strength value considered "strong"
327    pub strong_coupling: f64,
328    /// Minimum distance value considered "far"
329    pub far_distance: f64,
330    /// Minimum volatility value considered "high"
331    pub high_volatility: f64,
332    /// Number of dependencies to consider "high efferent coupling"
333    pub max_dependencies: usize,
334    /// Number of dependents to consider "high afferent coupling"
335    pub max_dependents: usize,
336}
337
338impl Default for IssueThresholds {
339    fn default() -> Self {
340        Self {
341            strong_coupling: 0.75, // Functional strength or higher (was 0.5)
342            far_distance: 0.5,     // DifferentModule or higher
343            high_volatility: 0.75, // High volatility only (was 0.5)
344            max_dependencies: 20,  // More than 20 outgoing dependencies (was 15)
345            max_dependents: 30,    // More than 30 incoming dependencies (was 20)
346        }
347    }
348}
349
350/// Crate stability classification
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum CrateStability {
353    /// Rust language fundamentals (std, core, alloc) - always ignore
354    Fundamental,
355    /// Highly stable, ubiquitous crates (serde, thiserror) - low concern
356    Stable,
357    /// Infrastructure crates (tokio, tracing) - medium concern
358    Infrastructure,
359    /// Regular external crate - normal analysis
360    Normal,
361}
362
363/// Check the stability classification of a crate
364pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
365    // Extract the base crate name (before ::)
366    let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
367
368    match base_name {
369        // Rust fundamentals - always safe to depend on
370        "std" | "core" | "alloc" => CrateStability::Fundamental,
371
372        // Highly stable, de-facto standard crates
373        "serde" | "serde_json" | "serde_yaml" | "toml" |  // Serialization
374        "thiserror" | "anyhow" |                           // Error handling
375        "log" |                                            // Logging trait
376        "chrono" | "time" |                                // Date/time
377        "uuid" |                                           // UUIDs
378        "regex" |                                          // Regex
379        "lazy_static" | "once_cell" |                      // Statics
380        "bytes" | "memchr" |                               // Byte utilities
381        "itertools" |                                      // Iterator utilities
382        "derive_more" | "strum"                            // Derive macros
383        => CrateStability::Stable,
384
385        // Infrastructure crates - stable but architectural decisions
386        "tokio" | "async-std" | "smol" |                   // Async runtimes
387        "async-trait" |                                    // Async traits
388        "futures" | "futures-util" |                       // Futures
389        "tracing" | "tracing-subscriber" |                 // Tracing
390        "tracing-opentelemetry" | "opentelemetry" |        // Observability
391        "opentelemetry-otlp" | "opentelemetry_sdk" |
392        "hyper" | "reqwest" | "http" |                     // HTTP
393        "tonic" | "prost" |                                // gRPC
394        "sqlx" | "diesel" | "sea-orm" |                    // Database
395        "clap" | "structopt"                               // CLI
396        => CrateStability::Infrastructure,
397
398        // Everything else
399        _ => CrateStability::Normal,
400    }
401}
402
403/// Check if a crate should be excluded from issue detection
404pub fn should_skip_crate(crate_name: &str) -> bool {
405    matches!(
406        classify_crate_stability(crate_name),
407        CrateStability::Fundamental
408    )
409}
410
411/// Check if a crate should have reduced severity
412pub fn should_reduce_severity(crate_name: &str) -> bool {
413    matches!(
414        classify_crate_stability(crate_name),
415        CrateStability::Stable | CrateStability::Infrastructure
416    )
417}
418
419/// Check if this is an external crate (not part of the workspace)
420/// External crates are identified by not containing "::" or starting with known external patterns
421pub fn is_external_crate(target: &str, source: &str) -> bool {
422    // If target doesn't have ::, it might be external
423    // But we need to check if it's the same workspace member
424
425    // Extract the crate/module prefix
426    let target_prefix = target.split("::").next().unwrap_or(target);
427    let source_prefix = source.split("::").next().unwrap_or(source);
428
429    // If they have the same prefix, it's internal
430    if target_prefix == source_prefix {
431        return false;
432    }
433
434    // If target looks like an external crate pattern (no workspace prefix match)
435    // Check if it's a known stable/infrastructure crate
436    let stability = classify_crate_stability(target);
437    matches!(
438        stability,
439        CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
440    )
441}
442
443/// Identify issues in a coupling relationship
444pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
445    identify_issues_with_thresholds(coupling, &IssueThresholds::default())
446}
447
448/// Identify issues with custom thresholds
449pub fn identify_issues_with_thresholds(
450    coupling: &CouplingMetrics,
451    _thresholds: &IssueThresholds,
452) -> Vec<CouplingIssue> {
453    let mut issues = Vec::new();
454
455    // Skip ALL external crate dependencies - we can't control them
456    // Focus only on internal workspace coupling issues
457    if coupling.distance == Distance::DifferentCrate {
458        return issues;
459    }
460
461    let balance = BalanceScore::calculate(coupling);
462
463    // Pattern 1: Global Complexity (INTRUSIVE coupling + different module)
464    // Only flag intrusive coupling across module boundaries - functional coupling is normal
465    if coupling.strength == IntegrationStrength::Intrusive
466        && coupling.distance == Distance::DifferentModule
467    {
468        issues.push(CouplingIssue {
469            issue_type: IssueType::GlobalComplexity,
470            severity: Severity::Medium, // Medium, not High - it's internal
471            source: coupling.source.clone(),
472            target: coupling.target.clone(),
473            description: format!(
474                "Intrusive coupling to {} across module boundary",
475                coupling.target,
476            ),
477            refactoring: RefactoringAction::IntroduceTrait {
478                suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
479                methods: vec!["// Extract required methods".to_string()],
480            },
481            balance_score: balance.score,
482        });
483    }
484
485    // Pattern 2: Cascading Change Risk (intrusive coupling + high volatility)
486    // Only flag if volatility is actually high (from Git data)
487    if coupling.strength == IntegrationStrength::Intrusive
488        && coupling.volatility == Volatility::High
489    {
490        issues.push(CouplingIssue {
491            issue_type: IssueType::CascadingChangeRisk,
492            severity: Severity::High,
493            source: coupling.source.clone(),
494            target: coupling.target.clone(),
495            description: format!(
496                "Intrusive coupling to frequently-changed component {}",
497                coupling.target,
498            ),
499            refactoring: RefactoringAction::StabilizeInterface {
500                interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
501            },
502            balance_score: balance.score,
503        });
504    }
505
506    // Pattern 3: Inappropriate Intimacy (Intrusive coupling across DIFFERENT MODULE only)
507    // Same module intrusive coupling is fine (it's cohesion)
508    if coupling.strength == IntegrationStrength::Intrusive
509        && coupling.distance == Distance::DifferentModule
510        && balance.score < 0.5
511    {
512        // Only add if not already added as GlobalComplexity
513        if !issues
514            .iter()
515            .any(|i| i.issue_type == IssueType::GlobalComplexity)
516        {
517            issues.push(CouplingIssue {
518                issue_type: IssueType::InappropriateIntimacy,
519                severity: Severity::Medium,
520                source: coupling.source.clone(),
521                target: coupling.target.clone(),
522                description: format!(
523                    "Direct internal access to {} across module boundary",
524                    coupling.target,
525                ),
526                refactoring: RefactoringAction::IntroduceTrait {
527                    suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
528                    methods: vec!["// Expose only necessary operations".to_string()],
529                },
530                balance_score: balance.score,
531            });
532        }
533    }
534
535    // Pattern 4: Unnecessary Abstraction - DISABLED
536    // This pattern generates too much noise and is rarely actionable
537    // Trait abstractions are generally good, even for nearby stable components
538
539    issues
540}
541
542/// Analyze all couplings in a project and identify issues
543pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
544    analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
545}
546
547/// Analyze all couplings with custom thresholds
548pub fn analyze_project_balance_with_thresholds(
549    metrics: &ProjectMetrics,
550    thresholds: &IssueThresholds,
551) -> ProjectBalanceReport {
552    let thresholds = thresholds.clone();
553    let mut all_issues = Vec::new();
554    let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
555    let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
556
557    // Analyze individual couplings
558    // Only INTERNAL couplings affect the health score
559    for coupling in &metrics.couplings {
560        let score = BalanceScore::calculate(coupling);
561        all_balance_scores.push(score.clone());
562
563        // Only count internal couplings for scoring
564        if coupling.distance != Distance::DifferentCrate {
565            internal_balance_scores.push(score);
566            let issues = identify_issues_with_thresholds(coupling, &thresholds);
567            all_issues.extend(issues);
568        }
569    }
570
571    // Analyze module-level coupling patterns (already filters external)
572    let module_issues = analyze_module_coupling(metrics, &thresholds);
573    all_issues.extend(module_issues);
574
575    // Sort by severity (critical first), then by balance score (worst first)
576    all_issues.sort_by(|a, b| {
577        b.severity
578            .cmp(&a.severity)
579            .then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
580    });
581
582    // Calculate summary statistics based on INTERNAL couplings only
583    let total_couplings = metrics.couplings.len();
584    let internal_couplings = internal_balance_scores.len();
585
586    let balanced_count = internal_balance_scores
587        .iter()
588        .filter(|s| s.is_balanced())
589        .count();
590    let needs_review = internal_balance_scores
591        .iter()
592        .filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
593        .count();
594    let needs_refactoring = internal_balance_scores
595        .iter()
596        .filter(|s| s.needs_refactoring())
597        .count();
598
599    // Average score based on internal couplings only
600    let average_score = if internal_balance_scores.is_empty() {
601        1.0 // No internal couplings = perfect score
602    } else {
603        internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
604            / internal_balance_scores.len() as f64
605    };
606
607    // Count issues by severity
608    let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
609    for issue in &all_issues {
610        *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
611    }
612
613    // Count issues by type
614    let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
615    for issue in &all_issues {
616        *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
617    }
618
619    // Determine overall health grade based on INTERNAL coupling issues
620    let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
621
622    ProjectBalanceReport {
623        total_couplings,
624        balanced_count,
625        needs_review,
626        needs_refactoring,
627        average_score,
628        health_grade,
629        issues_by_severity,
630        issues_by_type,
631        issues: all_issues,
632        top_priorities: Vec::new(), // Will be filled below
633    }
634    .with_top_priorities(5) // Increased from 3 to 5 for better actionability
635}
636
637/// Analyze module-level coupling (hub detection)
638fn analyze_module_coupling(
639    metrics: &ProjectMetrics,
640    thresholds: &IssueThresholds,
641) -> Vec<CouplingIssue> {
642    let mut issues = Vec::new();
643
644    // Count outgoing (efferent) and incoming (afferent) couplings per module
645    // Only count INTERNAL dependencies (within workspace), not external crates
646    let mut efferent: HashMap<&str, usize> = HashMap::new();
647    let mut afferent: HashMap<&str, usize> = HashMap::new();
648
649    for coupling in &metrics.couplings {
650        // Skip external crate dependencies entirely
651        if coupling.distance == Distance::DifferentCrate {
652            continue;
653        }
654
655        *efferent.entry(&coupling.source).or_insert(0) += 1;
656        *afferent.entry(&coupling.target).or_insert(0) += 1;
657    }
658
659    // Check for high efferent coupling (depends on too many things)
660    for (module, count) in &efferent {
661        if *count > thresholds.max_dependencies {
662            issues.push(CouplingIssue {
663                issue_type: IssueType::HighEfferentCoupling,
664                severity: if *count > thresholds.max_dependencies * 2 {
665                    Severity::High
666                } else {
667                    Severity::Medium
668                },
669                source: module.to_string(),
670                target: format!("{} dependencies", count),
671                description: format!(
672                    "Module {} depends on {} other components (threshold: {})",
673                    module, count, thresholds.max_dependencies
674                ),
675                refactoring: RefactoringAction::SplitModule {
676                    suggested_modules: vec![
677                        format!("{}_core", module),
678                        format!("{}_integration", module),
679                    ],
680                },
681                balance_score: 1.0
682                    - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
683            });
684        }
685    }
686
687    // Check for high afferent coupling (too many things depend on this)
688    // Only internal modules are counted (external crates already filtered above)
689    for (module, count) in &afferent {
690        if *count > thresholds.max_dependents {
691            issues.push(CouplingIssue {
692                issue_type: IssueType::HighAfferentCoupling,
693                severity: if *count > thresholds.max_dependents * 2 {
694                    Severity::High
695                } else {
696                    Severity::Medium
697                },
698                source: format!("{} dependents", count),
699                target: module.to_string(),
700                description: format!(
701                    "Module {} is depended on by {} other components (threshold: {})",
702                    module, count, thresholds.max_dependents
703                ),
704                refactoring: RefactoringAction::IntroduceTrait {
705                    suggested_name: format!("{}Interface", extract_type_name(module)),
706                    methods: vec!["// Define stable public API".to_string()],
707                },
708                balance_score: 1.0
709                    - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
710            });
711        }
712    }
713
714    issues
715}
716
717/// Health grade for the overall project
718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719pub enum HealthGrade {
720    A, // Excellent
721    B, // Good
722    C, // Acceptable
723    D, // Needs Improvement
724    F, // Critical Issues
725}
726
727impl std::fmt::Display for HealthGrade {
728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729        match self {
730            HealthGrade::A => write!(f, "A (Excellent)"),
731            HealthGrade::B => write!(f, "B (Good)"),
732            HealthGrade::C => write!(f, "C (Acceptable)"),
733            HealthGrade::D => write!(f, "D (Needs Improvement)"),
734            HealthGrade::F => write!(f, "F (Critical Issues)"),
735        }
736    }
737}
738
739/// Calculate health grade based on multiple quality factors
740///
741/// Unlike the previous version that only checked for issues,
742/// this version also considers positive quality indicators:
743/// - Contract coupling rate (trait usage)
744/// - Balance score distribution
745/// - Internal coupling complexity
746fn calculate_health_grade(
747    issues_by_severity: &HashMap<Severity, usize>,
748    internal_couplings: usize,
749) -> HealthGrade {
750    let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
751    let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
752    let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
753
754    // No internal couplings = B (not A - we can't assess quality without data)
755    if internal_couplings == 0 {
756        return HealthGrade::B;
757    }
758
759    // F: Multiple critical issues
760    if critical > 3 {
761        return HealthGrade::F;
762    }
763
764    // Calculate issue density (issues per internal coupling)
765    let high_density = high as f64 / internal_couplings as f64;
766    let medium_density = medium as f64 / internal_couplings as f64;
767    let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
768
769    // D: Critical issues or very high issue density (> 5% high)
770    if critical > 0 || high_density > 0.05 {
771        return HealthGrade::D;
772    }
773
774    // C: Any high issues OR high medium density (> 25%)
775    // Projects with structural issues that need attention
776    if high > 0 || medium_density > 0.25 {
777        return HealthGrade::C;
778    }
779
780    // B: Some medium issues but manageable (> 5% medium density)
781    if medium_density > 0.05 || total_issue_density > 0.10 {
782        return HealthGrade::B;
783    }
784
785    // A: Excellent - no high issues AND very low medium issues (< 5%)
786    // Reserved for exceptionally well-designed code
787    if high == 0 && medium_density <= 0.05 && internal_couplings >= 10 {
788        return HealthGrade::A;
789    }
790
791    // Default to B for projects with few issues
792    HealthGrade::B
793}
794
795/// Complete project balance analysis report
796#[derive(Debug)]
797pub struct ProjectBalanceReport {
798    pub total_couplings: usize,
799    pub balanced_count: usize,
800    pub needs_review: usize,
801    pub needs_refactoring: usize,
802    pub average_score: f64,
803    pub health_grade: HealthGrade,
804    pub issues_by_severity: HashMap<Severity, usize>,
805    pub issues_by_type: HashMap<IssueType, usize>,
806    pub issues: Vec<CouplingIssue>,
807    pub top_priorities: Vec<CouplingIssue>,
808}
809
810impl ProjectBalanceReport {
811    /// Add top N priority issues
812    fn with_top_priorities(mut self, n: usize) -> Self {
813        self.top_priorities = self.issues.iter().take(n).cloned().collect();
814        self
815    }
816
817    /// Get issues grouped by type
818    pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
819        let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
820        for issue in &self.issues {
821            grouped.entry(issue.issue_type).or_default().push(issue);
822        }
823        grouped
824    }
825}
826
827/// Calculate overall project balance score
828///
829/// Only considers internal couplings (not external crate dependencies)
830/// since external dependencies are outside the developer's control.
831pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
832    // Filter to internal couplings only
833    let internal_scores: Vec<f64> = metrics
834        .couplings
835        .iter()
836        .filter(|c| c.distance != Distance::DifferentCrate)
837        .map(|c| BalanceScore::calculate(c).score)
838        .collect();
839
840    if internal_scores.is_empty() {
841        return 1.0; // No internal couplings = perfect score
842    }
843
844    internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
845}
846
847// Helper functions
848
849fn extract_type_name(path: &str) -> String {
850    path.split("::")
851        .last()
852        .unwrap_or(path)
853        .chars()
854        .enumerate()
855        .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
856        .collect()
857}
858
859/// Get human-readable label for integration strength
860pub fn strength_label(strength: IntegrationStrength) -> &'static str {
861    match strength {
862        IntegrationStrength::Intrusive => "Intrusive",
863        IntegrationStrength::Functional => "Functional",
864        IntegrationStrength::Model => "Model",
865        IntegrationStrength::Contract => "Contract",
866    }
867}
868
869/// Get human-readable label for distance
870pub fn distance_label(distance: Distance) -> &'static str {
871    match distance {
872        Distance::SameFunction => "same function",
873        Distance::SameModule => "same module",
874        Distance::DifferentModule => "different module",
875        Distance::DifferentCrate => "external crate",
876    }
877}
878
879/// Get human-readable label for volatility
880pub fn volatility_label(volatility: Volatility) -> &'static str {
881    match volatility {
882        Volatility::Low => "rarely",
883        Volatility::Medium => "sometimes",
884        Volatility::High => "frequently",
885    }
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    fn make_coupling(
893        strength: IntegrationStrength,
894        distance: Distance,
895        volatility: Volatility,
896    ) -> CouplingMetrics {
897        CouplingMetrics::new(
898            "source::module".to_string(),
899            "target::module".to_string(),
900            strength,
901            distance,
902            volatility,
903        )
904    }
905
906    #[test]
907    fn test_balance_ideal_close() {
908        // Strong coupling + close distance = good (cohesion)
909        let coupling = make_coupling(
910            IntegrationStrength::Intrusive,
911            Distance::SameModule,
912            Volatility::Low,
913        );
914        let score = BalanceScore::calculate(&coupling);
915        assert!(score.is_balanced(), "Score: {}", score.score);
916    }
917
918    #[test]
919    fn test_balance_ideal_far() {
920        // Weak coupling + far distance = good (loose coupling)
921        let coupling = make_coupling(
922            IntegrationStrength::Contract,
923            Distance::DifferentCrate,
924            Volatility::Low,
925        );
926        let score = BalanceScore::calculate(&coupling);
927        assert!(score.is_balanced(), "Score: {}", score.score);
928    }
929
930    #[test]
931    fn test_balance_bad_global_complexity() {
932        // Strong coupling + far distance = bad (global complexity)
933        let coupling = make_coupling(
934            IntegrationStrength::Intrusive,
935            Distance::DifferentCrate,
936            Volatility::Low,
937        );
938        let score = BalanceScore::calculate(&coupling);
939        assert!(
940            score.needs_refactoring(),
941            "Score: {}, should need refactoring",
942            score.score
943        );
944    }
945
946    #[test]
947    fn test_balance_bad_cascading() {
948        // Strong coupling + high volatility = bad
949        let coupling = make_coupling(
950            IntegrationStrength::Intrusive,
951            Distance::SameModule,
952            Volatility::High,
953        );
954        let score = BalanceScore::calculate(&coupling);
955        assert!(
956            !score.is_balanced(),
957            "Score: {}, should not be balanced due to volatility",
958            score.score
959        );
960    }
961
962    #[test]
963    fn test_identify_global_complexity() {
964        // Note: DifferentCrate is now filtered out (external deps)
965        // Test with DifferentModule which is still flagged for internal modules
966        let coupling = make_coupling(
967            IntegrationStrength::Intrusive,
968            Distance::DifferentModule,
969            Volatility::Low,
970        );
971        let issues = identify_issues(&coupling);
972        assert!(
973            !issues.is_empty(),
974            "Should identify global complexity issue for internal cross-module coupling"
975        );
976        assert!(
977            issues
978                .iter()
979                .any(|i| i.issue_type == IssueType::GlobalComplexity)
980        );
981    }
982
983    #[test]
984    fn test_external_crates_are_skipped() {
985        // External crate dependencies should not generate issues
986        let coupling = make_coupling(
987            IntegrationStrength::Intrusive,
988            Distance::DifferentCrate,
989            Volatility::Low,
990        );
991        let issues = identify_issues(&coupling);
992        assert!(
993            issues.is_empty(),
994            "External crate dependencies should be skipped"
995        );
996    }
997
998    #[test]
999    fn test_identify_cascading_change() {
1000        // Now only INTRUSIVE + High volatility triggers cascading change risk
1001        let coupling = make_coupling(
1002            IntegrationStrength::Intrusive,
1003            Distance::SameModule,
1004            Volatility::High,
1005        );
1006        let issues = identify_issues(&coupling);
1007        assert!(
1008            issues
1009                .iter()
1010                .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1011            "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1012        );
1013    }
1014
1015    #[test]
1016    fn test_identify_inappropriate_intimacy() {
1017        // Intrusive + DifferentModule now detects GlobalComplexity (not InappropriateIntimacy)
1018        // because they overlap and GlobalComplexity takes precedence
1019        let coupling = make_coupling(
1020            IntegrationStrength::Intrusive,
1021            Distance::DifferentModule,
1022            Volatility::Low,
1023        );
1024        let issues = identify_issues(&coupling);
1025        assert!(
1026            issues
1027                .iter()
1028                .any(|i| i.issue_type == IssueType::GlobalComplexity),
1029            "Intrusive + DifferentModule should detect GlobalComplexity"
1030        );
1031    }
1032
1033    #[test]
1034    fn test_no_issues_for_balanced() {
1035        // Model coupling to different module with low volatility
1036        let coupling = make_coupling(
1037            IntegrationStrength::Model,
1038            Distance::DifferentModule,
1039            Volatility::Low,
1040        );
1041        let issues = identify_issues(&coupling);
1042        // Model coupling should have no issues (only Intrusive triggers issues)
1043        assert!(
1044            issues.is_empty(),
1045            "Model coupling should not generate issues"
1046        );
1047    }
1048
1049    #[test]
1050    fn test_health_grade_calculation() {
1051        let mut issues = HashMap::new();
1052
1053        // No issues with enough data = A (high == 0 && medium_density <= 0.05 && internal >= 10)
1054        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::A);
1055
1056        // No internal couplings = B (can't assess without data)
1057        assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1058
1059        // Any High issue = C (structural issues)
1060        issues.insert(Severity::High, 1);
1061        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1062
1063        // High density > 5% = D
1064        issues.clear();
1065        issues.insert(Severity::High, 6); // 6% of 100
1066        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1067
1068        // 1 Critical issue = D
1069        issues.clear();
1070        issues.insert(Severity::Critical, 1);
1071        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1072
1073        // 4+ Critical issues = F
1074        issues.clear();
1075        issues.insert(Severity::Critical, 4);
1076        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1077
1078        // Medium issues > 25% = C
1079        issues.clear();
1080        issues.insert(Severity::Medium, 30); // 30% of 100
1081        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1082
1083        // Medium issues > 5% but <= 25% = B
1084        issues.clear();
1085        issues.insert(Severity::Medium, 20); // 20% of 100
1086        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1087    }
1088}