Skip to main content

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    /// Exclude test code from function counts
391    pub exclude_tests: bool,
392    /// Prelude module patterns (for reporting purposes)
393    pub prelude_module_count: usize,
394}
395
396impl Default for IssueThresholds {
397    fn default() -> Self {
398        Self {
399            strong_coupling: 0.75,   // Functional strength or higher (was 0.5)
400            far_distance: 0.5,       // DifferentModule or higher
401            high_volatility: 0.75,   // High volatility only (was 0.5)
402            max_dependencies: 20,    // More than 20 outgoing dependencies (was 15)
403            max_dependents: 30,      // More than 30 incoming dependencies (was 20)
404            max_functions: 30,       // More than 30 functions = God Module
405            max_types: 15,           // More than 15 types = God Module
406            max_impls: 20,           // More than 20 implementations = God Module
407            min_primitive_params: 3, // 3+ primitive params = Primitive Obsession
408            strict_mode: true,       // Show only important issues by default
409            japanese: false,         // English by default
410            exclude_tests: false,    // Include test code by default
411            prelude_module_count: 0, // No prelude modules configured
412        }
413    }
414}
415
416/// Crate stability classification
417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub enum CrateStability {
419    /// Rust language fundamentals (std, core, alloc) - always ignore
420    Fundamental,
421    /// Highly stable, ubiquitous crates (serde, thiserror) - low concern
422    Stable,
423    /// Infrastructure crates (tokio, tracing) - medium concern
424    Infrastructure,
425    /// Regular external crate - normal analysis
426    Normal,
427}
428
429/// Check the stability classification of a crate
430pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
431    // Extract the base crate name (before ::)
432    let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
433
434    match base_name {
435        // Rust fundamentals - always safe to depend on
436        "std" | "core" | "alloc" => CrateStability::Fundamental,
437
438        // Highly stable, de-facto standard crates
439        "serde" | "serde_json" | "serde_yaml" | "toml" |  // Serialization
440        "thiserror" | "anyhow" |                           // Error handling
441        "log" |                                            // Logging trait
442        "chrono" | "time" |                                // Date/time
443        "uuid" |                                           // UUIDs
444        "regex" |                                          // Regex
445        "lazy_static" | "once_cell" |                      // Statics
446        "bytes" | "memchr" |                               // Byte utilities
447        "itertools" |                                      // Iterator utilities
448        "derive_more" | "strum"                            // Derive macros
449        => CrateStability::Stable,
450
451        // Infrastructure crates - stable but architectural decisions
452        "tokio" | "async-std" | "smol" |                   // Async runtimes
453        "async-trait" |                                    // Async traits
454        "futures" | "futures-util" |                       // Futures
455        "tracing" | "tracing-subscriber" |                 // Tracing
456        "tracing-opentelemetry" | "opentelemetry" |        // Observability
457        "opentelemetry-otlp" | "opentelemetry_sdk" |
458        "hyper" | "reqwest" | "http" |                     // HTTP
459        "tonic" | "prost" |                                // gRPC
460        "sqlx" | "diesel" | "sea-orm" |                    // Database
461        "clap" | "structopt"                               // CLI
462        => CrateStability::Infrastructure,
463
464        // Everything else
465        _ => CrateStability::Normal,
466    }
467}
468
469/// Check if a crate should be excluded from issue detection
470pub fn should_skip_crate(crate_name: &str) -> bool {
471    matches!(
472        classify_crate_stability(crate_name),
473        CrateStability::Fundamental
474    )
475}
476
477/// Check if a crate should have reduced severity
478pub fn should_reduce_severity(crate_name: &str) -> bool {
479    matches!(
480        classify_crate_stability(crate_name),
481        CrateStability::Stable | CrateStability::Infrastructure
482    )
483}
484
485/// Check if this is an external crate (not part of the workspace)
486/// External crates are identified by not containing "::" or starting with known external patterns
487pub fn is_external_crate(target: &str, source: &str) -> bool {
488    // If target doesn't have ::, it might be external
489    // But we need to check if it's the same workspace member
490
491    // Extract the crate/module prefix
492    let target_prefix = target.split("::").next().unwrap_or(target);
493    let source_prefix = source.split("::").next().unwrap_or(source);
494
495    // If they have the same prefix, it's internal
496    if target_prefix == source_prefix {
497        return false;
498    }
499
500    // If target looks like an external crate pattern (no workspace prefix match)
501    // Check if it's a known stable/infrastructure crate
502    let stability = classify_crate_stability(target);
503    matches!(
504        stability,
505        CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
506    )
507}
508
509/// Identify issues in a coupling relationship
510pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
511    identify_issues_with_thresholds(coupling, &IssueThresholds::default())
512}
513
514/// Identify issues with custom thresholds
515pub fn identify_issues_with_thresholds(
516    coupling: &CouplingMetrics,
517    _thresholds: &IssueThresholds,
518) -> Vec<CouplingIssue> {
519    let mut issues = Vec::new();
520
521    // Skip ALL external crate dependencies - we can't control them
522    // Focus only on internal workspace coupling issues
523    if coupling.distance == Distance::DifferentCrate {
524        return issues;
525    }
526
527    let balance = BalanceScore::calculate(coupling);
528
529    // Pattern 1: Global Complexity (INTRUSIVE coupling + different module)
530    // Only flag intrusive coupling across module boundaries - functional coupling is normal
531    if coupling.strength == IntegrationStrength::Intrusive
532        && coupling.distance == Distance::DifferentModule
533    {
534        issues.push(CouplingIssue {
535            issue_type: IssueType::GlobalComplexity,
536            severity: Severity::Medium, // Medium, not High - it's internal
537            source: coupling.source.clone(),
538            target: coupling.target.clone(),
539            description: format!(
540                "Intrusive coupling to {} across module boundary",
541                coupling.target,
542            ),
543            refactoring: RefactoringAction::IntroduceTrait {
544                suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
545                methods: vec!["// Extract required methods".to_string()],
546            },
547            balance_score: balance.score,
548        });
549    }
550
551    // Pattern 2: Cascading Change Risk (intrusive coupling + high volatility)
552    // Only flag if volatility is actually high (from Git data)
553    if coupling.strength == IntegrationStrength::Intrusive
554        && coupling.volatility == Volatility::High
555    {
556        issues.push(CouplingIssue {
557            issue_type: IssueType::CascadingChangeRisk,
558            severity: Severity::High,
559            source: coupling.source.clone(),
560            target: coupling.target.clone(),
561            description: format!(
562                "Intrusive coupling to frequently-changed component {}",
563                coupling.target,
564            ),
565            refactoring: RefactoringAction::StabilizeInterface {
566                interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
567            },
568            balance_score: balance.score,
569        });
570    }
571
572    // Pattern 3: Inappropriate Intimacy (Intrusive coupling across DIFFERENT MODULE only)
573    // Same module intrusive coupling is fine (it's cohesion)
574    if coupling.strength == IntegrationStrength::Intrusive
575        && coupling.distance == Distance::DifferentModule
576        && balance.score < 0.5
577    {
578        // Only add if not already added as GlobalComplexity
579        if !issues
580            .iter()
581            .any(|i| i.issue_type == IssueType::GlobalComplexity)
582        {
583            issues.push(CouplingIssue {
584                issue_type: IssueType::InappropriateIntimacy,
585                severity: Severity::Medium,
586                source: coupling.source.clone(),
587                target: coupling.target.clone(),
588                description: format!(
589                    "Direct internal access to {} across module boundary",
590                    coupling.target,
591                ),
592                refactoring: RefactoringAction::IntroduceTrait {
593                    suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
594                    methods: vec!["// Expose only necessary operations".to_string()],
595                },
596                balance_score: balance.score,
597            });
598        }
599    }
600
601    // Pattern 4: Unnecessary Abstraction - DISABLED
602    // This pattern generates too much noise and is rarely actionable
603    // Trait abstractions are generally good, even for nearby stable components
604
605    issues
606}
607
608/// Analyze all couplings in a project and identify issues
609pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
610    analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
611}
612
613/// Analyze all couplings with custom thresholds
614pub fn analyze_project_balance_with_thresholds(
615    metrics: &ProjectMetrics,
616    thresholds: &IssueThresholds,
617) -> ProjectBalanceReport {
618    let thresholds = thresholds.clone();
619    let mut all_issues = Vec::new();
620    let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
621    let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
622
623    // Analyze individual couplings
624    // Only INTERNAL couplings affect the health score
625    for coupling in &metrics.couplings {
626        let score = BalanceScore::calculate(coupling);
627        all_balance_scores.push(score.clone());
628
629        // Only count internal couplings for scoring
630        if coupling.distance != Distance::DifferentCrate {
631            internal_balance_scores.push(score);
632            let issues = identify_issues_with_thresholds(coupling, &thresholds);
633            all_issues.extend(issues);
634        }
635    }
636
637    // Analyze module-level coupling patterns (already filters external)
638    let module_issues = analyze_module_coupling(metrics, &thresholds);
639    all_issues.extend(module_issues);
640
641    // Analyze Khononov/Rust-specific issues
642    let rust_issues = analyze_rust_patterns(metrics, &thresholds);
643    all_issues.extend(rust_issues);
644
645    // Strict mode: filter out Low severity issues to reduce noise
646    if thresholds.strict_mode {
647        all_issues.retain(|issue| issue.severity >= Severity::Medium);
648    }
649
650    // Sort by severity (critical first), then by balance score (worst first)
651    all_issues.sort_by(|a, b| {
652        b.severity
653            .cmp(&a.severity)
654            .then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
655    });
656
657    // Calculate summary statistics based on INTERNAL couplings only
658    let total_couplings = metrics.couplings.len();
659    let internal_couplings = internal_balance_scores.len();
660
661    let balanced_count = internal_balance_scores
662        .iter()
663        .filter(|s| s.is_balanced())
664        .count();
665    let needs_review = internal_balance_scores
666        .iter()
667        .filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
668        .count();
669    let needs_refactoring = internal_balance_scores
670        .iter()
671        .filter(|s| s.needs_refactoring())
672        .count();
673
674    // Average score based on internal couplings only
675    let average_score = if internal_balance_scores.is_empty() {
676        1.0 // No internal couplings = perfect score
677    } else {
678        internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
679            / internal_balance_scores.len() as f64
680    };
681
682    // Count issues by severity
683    let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
684    for issue in &all_issues {
685        *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
686    }
687
688    // Count issues by type
689    let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
690    for issue in &all_issues {
691        *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
692    }
693
694    // Determine overall health grade based on INTERNAL coupling issues
695    let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
696
697    ProjectBalanceReport {
698        total_couplings,
699        balanced_count,
700        needs_review,
701        needs_refactoring,
702        average_score,
703        health_grade,
704        issues_by_severity,
705        issues_by_type,
706        issues: all_issues,
707        top_priorities: Vec::new(), // Will be filled below
708    }
709    .with_top_priorities(5) // Increased from 3 to 5 for better actionability
710}
711
712/// Analyze module-level coupling (hub detection)
713fn analyze_module_coupling(
714    metrics: &ProjectMetrics,
715    thresholds: &IssueThresholds,
716) -> Vec<CouplingIssue> {
717    let mut issues = Vec::new();
718
719    // Count outgoing (efferent) and incoming (afferent) couplings per module
720    // Only count INTERNAL dependencies (within workspace), not external crates
721    let mut efferent: HashMap<&str, usize> = HashMap::new();
722    let mut afferent: HashMap<&str, usize> = HashMap::new();
723
724    for coupling in &metrics.couplings {
725        // Skip external crate dependencies entirely
726        if coupling.distance == Distance::DifferentCrate {
727            continue;
728        }
729
730        *efferent.entry(&coupling.source).or_insert(0) += 1;
731        *afferent.entry(&coupling.target).or_insert(0) += 1;
732    }
733
734    // Check for high efferent coupling (depends on too many things)
735    for (module, count) in &efferent {
736        if *count > thresholds.max_dependencies {
737            issues.push(CouplingIssue {
738                issue_type: IssueType::HighEfferentCoupling,
739                severity: if *count > thresholds.max_dependencies * 2 {
740                    Severity::High
741                } else {
742                    Severity::Medium
743                },
744                source: module.to_string(),
745                target: format!("{} dependencies", count),
746                description: format!(
747                    "Module {} depends on {} other components (threshold: {})",
748                    module, count, thresholds.max_dependencies
749                ),
750                refactoring: RefactoringAction::SplitModule {
751                    suggested_modules: vec![
752                        format!("{}_core", module),
753                        format!("{}_integration", module),
754                    ],
755                },
756                balance_score: 1.0
757                    - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
758            });
759        }
760    }
761
762    // Check for high afferent coupling (too many things depend on this)
763    // Only internal modules are counted (external crates already filtered above)
764    for (module, count) in &afferent {
765        if *count > thresholds.max_dependents {
766            issues.push(CouplingIssue {
767                issue_type: IssueType::HighAfferentCoupling,
768                severity: if *count > thresholds.max_dependents * 2 {
769                    Severity::High
770                } else {
771                    Severity::Medium
772                },
773                source: format!("{} dependents", count),
774                target: module.to_string(),
775                description: format!(
776                    "Module {} is depended on by {} other components (threshold: {})",
777                    module, count, thresholds.max_dependents
778                ),
779                refactoring: RefactoringAction::IntroduceTrait {
780                    suggested_name: format!("{}Interface", extract_type_name(module)),
781                    methods: vec!["// Define stable public API".to_string()],
782                },
783                balance_score: 1.0
784                    - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
785            });
786        }
787    }
788
789    issues
790}
791
792/// Analyze Rust-specific patterns (God Module, Public Field Exposure, Primitive Obsession)
793fn analyze_rust_patterns(
794    metrics: &ProjectMetrics,
795    thresholds: &IssueThresholds,
796) -> Vec<CouplingIssue> {
797    let mut issues = Vec::new();
798
799    // God Module detection
800    for (module_name, module) in &metrics.modules {
801        // Calculate function count, excluding test functions if configured
802        let func_count = if thresholds.exclude_tests {
803            module
804                .function_count()
805                .saturating_sub(module.test_function_count)
806        } else {
807            module.function_count()
808        };
809        let type_count = module.type_definitions.len();
810        let impl_count = module.trait_impl_count + module.inherent_impl_count;
811
812        // Check if module exceeds thresholds (with test exclusion applied)
813        let is_god_module = func_count > thresholds.max_functions
814            || type_count > thresholds.max_types
815            || impl_count > thresholds.max_impls;
816
817        if is_god_module {
818            issues.push(CouplingIssue {
819                issue_type: IssueType::GodModule,
820                severity: if func_count > thresholds.max_functions * 2
821                    || type_count > thresholds.max_types * 2
822                {
823                    Severity::High
824                } else {
825                    Severity::Medium
826                },
827                source: module_name.clone(),
828                target: format!(
829                    "{} functions, {} types, {} impls",
830                    func_count, type_count, impl_count
831                ),
832                description: format!(
833                    "Module {} has too many responsibilities (functions: {}/{}, types: {}/{}, impls: {}/{})",
834                    module_name,
835                    func_count, thresholds.max_functions,
836                    type_count, thresholds.max_types,
837                    impl_count, thresholds.max_impls,
838                ),
839                refactoring: RefactoringAction::SplitModule {
840                    suggested_modules: vec![
841                        format!("{}_core", module_name),
842                        format!("{}_helpers", module_name),
843                    ],
844                },
845                balance_score: 0.5,
846            });
847        }
848
849        // Public Field Exposure detection
850        for type_def in module.type_definitions.values() {
851            if type_def.public_field_count > 0
852                && !type_def.is_trait
853                && type_def.visibility == crate::metrics::Visibility::Public
854            {
855                issues.push(CouplingIssue {
856                    issue_type: IssueType::PublicFieldExposure,
857                    severity: Severity::Low,
858                    source: format!("{}::{}", module_name, type_def.name),
859                    target: format!("{} public fields", type_def.public_field_count),
860                    description: format!(
861                        "Type {} has {} public field(s). Consider using getter methods.",
862                        type_def.name, type_def.public_field_count
863                    ),
864                    refactoring: RefactoringAction::AddGetters {
865                        fields: vec!["// Add getter methods".to_string()],
866                    },
867                    balance_score: 0.7,
868                });
869            }
870        }
871
872        // Primitive Obsession detection
873        for func_def in module.function_definitions.values() {
874            if func_def.primitive_param_count >= thresholds.min_primitive_params
875                && func_def.param_count >= thresholds.min_primitive_params
876            {
877                let ratio = func_def.primitive_param_count as f64 / func_def.param_count as f64;
878                if ratio >= 0.6 {
879                    issues.push(CouplingIssue {
880                        issue_type: IssueType::PrimitiveObsession,
881                        severity: Severity::Low,
882                        source: format!("{}::{}", module_name, func_def.name),
883                        target: format!(
884                            "{}/{} primitive params",
885                            func_def.primitive_param_count, func_def.param_count
886                        ),
887                        description: format!(
888                            "Function {} has {} primitive parameters. Consider newtype pattern.",
889                            func_def.name, func_def.primitive_param_count
890                        ),
891                        refactoring: RefactoringAction::IntroduceNewtype {
892                            suggested_name: format!("{}Params", capitalize_first(&func_def.name)),
893                            wrapped_type: "// Group related parameters".to_string(),
894                        },
895                        balance_score: 0.7,
896                    });
897                }
898            }
899        }
900    }
901
902    issues
903}
904
905/// Capitalize first letter of a string
906fn capitalize_first(s: &str) -> String {
907    let mut chars = s.chars();
908    match chars.next() {
909        None => String::new(),
910        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
911    }
912}
913
914/// Health grade for the overall project
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub enum HealthGrade {
917    S, // Over-optimized - stop! you're doing too much
918    A, // Well-balanced - coupling is appropriate for the architecture
919    B, // Healthy - minor issues exist but manageable
920    C, // Room for improvement - some structural issues
921    D, // Attention needed - significant issues affecting maintainability
922    F, // Immediate action required - critical issues blocking development
923}
924
925impl std::fmt::Display for HealthGrade {
926    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
927        match self {
928            HealthGrade::S => write!(f, "S (Over-optimized! Real code has some issues. Ship it!)"),
929            HealthGrade::A => write!(f, "A (Well-balanced)"),
930            HealthGrade::B => write!(f, "B (Healthy)"),
931            HealthGrade::C => write!(f, "C (Room for improvement)"),
932            HealthGrade::D => write!(f, "D (Attention needed)"),
933            HealthGrade::F => write!(f, "F (Immediate action required)"),
934        }
935    }
936}
937
938/// Calculate health grade based on multiple quality factors
939///
940/// Unlike the previous version that only checked for issues,
941/// this version also considers positive quality indicators:
942/// - Contract coupling rate (trait usage)
943/// - Balance score distribution
944/// - Internal coupling complexity
945fn calculate_health_grade(
946    issues_by_severity: &HashMap<Severity, usize>,
947    internal_couplings: usize,
948) -> HealthGrade {
949    let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
950    let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
951    let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
952
953    // No internal couplings = B (not A - we can't assess quality without data)
954    if internal_couplings == 0 {
955        return HealthGrade::B;
956    }
957
958    // F: Multiple critical issues
959    if critical > 3 {
960        return HealthGrade::F;
961    }
962
963    // Calculate issue density (issues per internal coupling)
964    let high_density = high as f64 / internal_couplings as f64;
965    let medium_density = medium as f64 / internal_couplings as f64;
966    let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
967
968    // D: Critical issues or very high issue density (> 5% high)
969    if critical > 0 || high_density > 0.05 {
970        return HealthGrade::D;
971    }
972
973    // C: Any high issues OR high medium density (> 25%)
974    // Projects with structural issues that need attention
975    if high > 0 || medium_density > 0.25 {
976        return HealthGrade::C;
977    }
978
979    // B: Some medium issues but manageable (> 10% medium density)
980    if medium_density > 0.10 || total_issue_density > 0.15 {
981        return HealthGrade::B;
982    }
983
984    // S: Over-optimized! Too few issues (< 5%) = you're probably over-engineering
985    // This is a WARNING, not a reward. Stop refactoring!
986    if high == 0 && medium_density <= 0.05 && internal_couplings >= 20 {
987        return HealthGrade::S;
988    }
989
990    // A: Well-balanced - no high issues AND reasonable medium issues (5-10%)
991    // This is the ideal target grade
992    if high == 0 && medium_density <= 0.10 && internal_couplings >= 10 {
993        return HealthGrade::A;
994    }
995
996    // Default to B for projects with few issues
997    HealthGrade::B
998}
999
1000/// Complete project balance analysis report
1001#[derive(Debug)]
1002pub struct ProjectBalanceReport {
1003    pub total_couplings: usize,
1004    pub balanced_count: usize,
1005    pub needs_review: usize,
1006    pub needs_refactoring: usize,
1007    pub average_score: f64,
1008    pub health_grade: HealthGrade,
1009    pub issues_by_severity: HashMap<Severity, usize>,
1010    pub issues_by_type: HashMap<IssueType, usize>,
1011    pub issues: Vec<CouplingIssue>,
1012    pub top_priorities: Vec<CouplingIssue>,
1013}
1014
1015impl ProjectBalanceReport {
1016    /// Add top N priority issues
1017    fn with_top_priorities(mut self, n: usize) -> Self {
1018        self.top_priorities = self.issues.iter().take(n).cloned().collect();
1019        self
1020    }
1021
1022    /// Get issues grouped by type
1023    pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
1024        let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
1025        for issue in &self.issues {
1026            grouped.entry(issue.issue_type).or_default().push(issue);
1027        }
1028        grouped
1029    }
1030}
1031
1032/// Calculate overall project balance score
1033///
1034/// Only considers internal couplings (not external crate dependencies)
1035/// since external dependencies are outside the developer's control.
1036pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
1037    // Filter to internal couplings only
1038    let internal_scores: Vec<f64> = metrics
1039        .couplings
1040        .iter()
1041        .filter(|c| c.distance != Distance::DifferentCrate)
1042        .map(|c| BalanceScore::calculate(c).score)
1043        .collect();
1044
1045    if internal_scores.is_empty() {
1046        return 1.0; // No internal couplings = perfect score
1047    }
1048
1049    internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
1050}
1051
1052// Helper functions
1053
1054fn extract_type_name(path: &str) -> String {
1055    path.split("::")
1056        .last()
1057        .unwrap_or(path)
1058        .chars()
1059        .enumerate()
1060        .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
1061        .collect()
1062}
1063
1064/// Get human-readable label for integration strength
1065pub fn strength_label(strength: IntegrationStrength) -> &'static str {
1066    match strength {
1067        IntegrationStrength::Intrusive => "Intrusive",
1068        IntegrationStrength::Functional => "Functional",
1069        IntegrationStrength::Model => "Model",
1070        IntegrationStrength::Contract => "Contract",
1071    }
1072}
1073
1074/// Get human-readable label for distance
1075pub fn distance_label(distance: Distance) -> &'static str {
1076    match distance {
1077        Distance::SameFunction => "same function",
1078        Distance::SameModule => "same module",
1079        Distance::DifferentModule => "different module",
1080        Distance::DifferentCrate => "external crate",
1081    }
1082}
1083
1084/// Get human-readable label for volatility
1085pub fn volatility_label(volatility: Volatility) -> &'static str {
1086    match volatility {
1087        Volatility::Low => "rarely",
1088        Volatility::Medium => "sometimes",
1089        Volatility::High => "frequently",
1090    }
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096
1097    fn make_coupling(
1098        strength: IntegrationStrength,
1099        distance: Distance,
1100        volatility: Volatility,
1101    ) -> CouplingMetrics {
1102        CouplingMetrics::new(
1103            "source::module".to_string(),
1104            "target::module".to_string(),
1105            strength,
1106            distance,
1107            volatility,
1108        )
1109    }
1110
1111    #[test]
1112    fn test_balance_ideal_close() {
1113        // Strong coupling + close distance = good (cohesion)
1114        let coupling = make_coupling(
1115            IntegrationStrength::Intrusive,
1116            Distance::SameModule,
1117            Volatility::Low,
1118        );
1119        let score = BalanceScore::calculate(&coupling);
1120        assert!(score.is_balanced(), "Score: {}", score.score);
1121    }
1122
1123    #[test]
1124    fn test_balance_ideal_far() {
1125        // Weak coupling + far distance = good (loose coupling)
1126        let coupling = make_coupling(
1127            IntegrationStrength::Contract,
1128            Distance::DifferentCrate,
1129            Volatility::Low,
1130        );
1131        let score = BalanceScore::calculate(&coupling);
1132        assert!(score.is_balanced(), "Score: {}", score.score);
1133    }
1134
1135    #[test]
1136    fn test_balance_bad_global_complexity() {
1137        // Strong coupling + far distance = bad (global complexity)
1138        let coupling = make_coupling(
1139            IntegrationStrength::Intrusive,
1140            Distance::DifferentCrate,
1141            Volatility::Low,
1142        );
1143        let score = BalanceScore::calculate(&coupling);
1144        assert!(
1145            score.needs_refactoring(),
1146            "Score: {}, should need refactoring",
1147            score.score
1148        );
1149    }
1150
1151    #[test]
1152    fn test_balance_bad_cascading() {
1153        // Strong coupling + high volatility = bad
1154        let coupling = make_coupling(
1155            IntegrationStrength::Intrusive,
1156            Distance::SameModule,
1157            Volatility::High,
1158        );
1159        let score = BalanceScore::calculate(&coupling);
1160        assert!(
1161            !score.is_balanced(),
1162            "Score: {}, should not be balanced due to volatility",
1163            score.score
1164        );
1165    }
1166
1167    #[test]
1168    fn test_identify_global_complexity() {
1169        // Note: DifferentCrate is now filtered out (external deps)
1170        // Test with DifferentModule which is still flagged for internal modules
1171        let coupling = make_coupling(
1172            IntegrationStrength::Intrusive,
1173            Distance::DifferentModule,
1174            Volatility::Low,
1175        );
1176        let issues = identify_issues(&coupling);
1177        assert!(
1178            !issues.is_empty(),
1179            "Should identify global complexity issue for internal cross-module coupling"
1180        );
1181        assert!(
1182            issues
1183                .iter()
1184                .any(|i| i.issue_type == IssueType::GlobalComplexity)
1185        );
1186    }
1187
1188    #[test]
1189    fn test_external_crates_are_skipped() {
1190        // External crate dependencies should not generate issues
1191        let coupling = make_coupling(
1192            IntegrationStrength::Intrusive,
1193            Distance::DifferentCrate,
1194            Volatility::Low,
1195        );
1196        let issues = identify_issues(&coupling);
1197        assert!(
1198            issues.is_empty(),
1199            "External crate dependencies should be skipped"
1200        );
1201    }
1202
1203    #[test]
1204    fn test_identify_cascading_change() {
1205        // Now only INTRUSIVE + High volatility triggers cascading change risk
1206        let coupling = make_coupling(
1207            IntegrationStrength::Intrusive,
1208            Distance::SameModule,
1209            Volatility::High,
1210        );
1211        let issues = identify_issues(&coupling);
1212        assert!(
1213            issues
1214                .iter()
1215                .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1216            "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1217        );
1218    }
1219
1220    #[test]
1221    fn test_identify_inappropriate_intimacy() {
1222        // Intrusive + DifferentModule now detects GlobalComplexity (not InappropriateIntimacy)
1223        // because they overlap and GlobalComplexity takes precedence
1224        let coupling = make_coupling(
1225            IntegrationStrength::Intrusive,
1226            Distance::DifferentModule,
1227            Volatility::Low,
1228        );
1229        let issues = identify_issues(&coupling);
1230        assert!(
1231            issues
1232                .iter()
1233                .any(|i| i.issue_type == IssueType::GlobalComplexity),
1234            "Intrusive + DifferentModule should detect GlobalComplexity"
1235        );
1236    }
1237
1238    #[test]
1239    fn test_no_issues_for_balanced() {
1240        // Model coupling to different module with low volatility
1241        let coupling = make_coupling(
1242            IntegrationStrength::Model,
1243            Distance::DifferentModule,
1244            Volatility::Low,
1245        );
1246        let issues = identify_issues(&coupling);
1247        // Model coupling should have no issues (only Intrusive triggers issues)
1248        assert!(
1249            issues.is_empty(),
1250            "Model coupling should not generate issues"
1251        );
1252    }
1253
1254    #[test]
1255    fn test_health_grade_calculation() {
1256        let mut issues = HashMap::new();
1257
1258        // No issues with >= 20 couplings = S (over-optimized warning)
1259        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::S);
1260
1261        // No issues with 10-19 couplings = A (well-balanced)
1262        assert_eq!(calculate_health_grade(&issues, 15), HealthGrade::A);
1263
1264        // No internal couplings = B (can't assess without data)
1265        assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1266
1267        // Any High issue = C (structural issues)
1268        issues.insert(Severity::High, 1);
1269        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1270
1271        // High density > 5% = D
1272        issues.clear();
1273        issues.insert(Severity::High, 6); // 6% of 100
1274        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1275
1276        // 1 Critical issue = D
1277        issues.clear();
1278        issues.insert(Severity::Critical, 1);
1279        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1280
1281        // 4+ Critical issues = F
1282        issues.clear();
1283        issues.insert(Severity::Critical, 4);
1284        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1285
1286        // Medium issues > 25% = C
1287        issues.clear();
1288        issues.insert(Severity::Medium, 30); // 30% of 100
1289        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1290
1291        // Medium issues > 5% but <= 25% = B
1292        issues.clear();
1293        issues.insert(Severity::Medium, 20); // 20% of 100
1294        assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1295    }
1296}