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