1use std::collections::HashMap;
13
14use crate::metrics::{CouplingMetrics, Distance, IntegrationStrength, ProjectMetrics, Volatility};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Severity {
19 Low,
21 Medium,
23 High,
25 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum IssueType {
43 GlobalComplexity,
45 CascadingChangeRisk,
47 InappropriateIntimacy,
49 HighEfferentCoupling,
51 HighAfferentCoupling,
53 UnnecessaryAbstraction,
55 CircularDependency,
57
58 ShallowModule,
61 PassThroughMethod,
63 HighCognitiveLoad,
65}
66
67impl std::fmt::Display for IssueType {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 IssueType::GlobalComplexity => write!(f, "Global Complexity"),
71 IssueType::CascadingChangeRisk => write!(f, "Cascading Change Risk"),
72 IssueType::InappropriateIntimacy => write!(f, "Inappropriate Intimacy"),
73 IssueType::HighEfferentCoupling => write!(f, "High Efferent Coupling"),
74 IssueType::HighAfferentCoupling => write!(f, "High Afferent Coupling"),
75 IssueType::UnnecessaryAbstraction => write!(f, "Unnecessary Abstraction"),
76 IssueType::CircularDependency => write!(f, "Circular Dependency"),
77 IssueType::ShallowModule => write!(f, "Shallow Module"),
79 IssueType::PassThroughMethod => write!(f, "Pass-Through Method"),
80 IssueType::HighCognitiveLoad => write!(f, "High Cognitive Load"),
81 }
82 }
83}
84
85impl IssueType {
86 pub fn description(&self) -> &'static str {
88 match self {
89 IssueType::GlobalComplexity => {
90 "Strong coupling to distant components increases cognitive load and makes the system harder to understand and modify."
91 }
92 IssueType::CascadingChangeRisk => {
93 "Strongly coupling to volatile components means changes will cascade through the system, requiring updates in many places."
94 }
95 IssueType::InappropriateIntimacy => {
96 "Direct access to internal details (fields, private methods) across module boundaries violates encapsulation."
97 }
98 IssueType::HighEfferentCoupling => {
99 "A module depending on too many others is fragile and hard to test. Changes anywhere affect this module."
100 }
101 IssueType::HighAfferentCoupling => {
102 "A module that many others depend on is hard to change. Any modification risks breaking dependents."
103 }
104 IssueType::UnnecessaryAbstraction => {
105 "Using abstract interfaces for closely-related stable components may add complexity without benefit."
106 }
107 IssueType::CircularDependency => {
108 "Circular dependencies make it impossible to understand, test, or modify components in isolation."
109 }
110 IssueType::ShallowModule => {
112 "Interface complexity is close to implementation complexity. The module doesn't hide enough complexity behind a simple interface. (APOSD: Deep vs Shallow Modules)"
113 }
114 IssueType::PassThroughMethod => {
115 "Method only delegates to another method without adding significant functionality. Indicates unclear responsibility division. (APOSD: Pass-Through Methods)"
116 }
117 IssueType::HighCognitiveLoad => {
118 "Module requires too much knowledge to understand and modify. Too many public APIs, dependencies, or complex type signatures. (APOSD: Cognitive Load)"
119 }
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct CouplingIssue {
127 pub issue_type: IssueType,
129 pub severity: Severity,
131 pub source: String,
133 pub target: String,
135 pub description: String,
137 pub refactoring: RefactoringAction,
139 pub balance_score: f64,
141}
142
143#[derive(Debug, Clone)]
145pub enum RefactoringAction {
146 IntroduceTrait {
148 suggested_name: String,
149 methods: Vec<String>,
150 },
151 MoveCloser { target_location: String },
153 ExtractAdapter {
155 adapter_name: String,
156 purpose: String,
157 },
158 SplitModule { suggested_modules: Vec<String> },
160 SimplifyAbstraction { direct_usage: String },
162 BreakCycle { suggested_direction: String },
164 StabilizeInterface { interface_name: String },
166 General { action: String },
168}
169
170impl std::fmt::Display for RefactoringAction {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 match self {
173 RefactoringAction::IntroduceTrait {
174 suggested_name,
175 methods,
176 } => {
177 write!(
178 f,
179 "Introduce trait `{}` with methods: {}",
180 suggested_name,
181 methods.join(", ")
182 )
183 }
184 RefactoringAction::MoveCloser { target_location } => {
185 write!(f, "Move component to `{}`", target_location)
186 }
187 RefactoringAction::ExtractAdapter {
188 adapter_name,
189 purpose,
190 } => {
191 write!(f, "Extract adapter `{}` to {}", adapter_name, purpose)
192 }
193 RefactoringAction::SplitModule { suggested_modules } => {
194 write!(f, "Split into modules: {}", suggested_modules.join(", "))
195 }
196 RefactoringAction::SimplifyAbstraction { direct_usage } => {
197 write!(f, "Replace with direct usage: {}", direct_usage)
198 }
199 RefactoringAction::BreakCycle {
200 suggested_direction,
201 } => {
202 write!(f, "Break cycle by {}", suggested_direction)
203 }
204 RefactoringAction::StabilizeInterface { interface_name } => {
205 write!(f, "Add stable interface `{}`", interface_name)
206 }
207 RefactoringAction::General { action } => {
208 write!(f, "{}", action)
209 }
210 }
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct BalanceScore {
217 pub coupling: CouplingMetrics,
219 pub score: f64,
221 pub alignment: f64,
223 pub volatility_impact: f64,
225 pub interpretation: BalanceInterpretation,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum BalanceInterpretation {
232 Balanced,
234 Acceptable,
236 NeedsReview,
238 NeedsRefactoring,
240 Critical,
242}
243
244impl std::fmt::Display for BalanceInterpretation {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self {
247 BalanceInterpretation::Balanced => write!(f, "Balanced"),
248 BalanceInterpretation::Acceptable => write!(f, "Acceptable"),
249 BalanceInterpretation::NeedsReview => write!(f, "Needs Review"),
250 BalanceInterpretation::NeedsRefactoring => write!(f, "Needs Refactoring"),
251 BalanceInterpretation::Critical => write!(f, "Critical"),
252 }
253 }
254}
255
256impl BalanceScore {
257 pub fn calculate(coupling: &CouplingMetrics) -> Self {
269 let strength = coupling.strength_value();
270 let distance = coupling.distance_value();
271 let volatility = coupling.volatility_value();
272
273 let alignment = 1.0 - (strength - (1.0 - distance)).abs();
278
279 let volatility_penalty = volatility * strength;
282 let volatility_impact = 1.0 - volatility_penalty;
283
284 let score = alignment * volatility_impact;
287
288 let interpretation = match score {
290 s if s >= 0.8 => BalanceInterpretation::Balanced,
291 s if s >= 0.6 => BalanceInterpretation::Acceptable,
292 s if s >= 0.4 => BalanceInterpretation::NeedsReview,
293 s if s >= 0.2 => BalanceInterpretation::NeedsRefactoring,
294 _ => BalanceInterpretation::Critical,
295 };
296
297 Self {
298 coupling: coupling.clone(),
299 score,
300 alignment,
301 volatility_impact,
302 interpretation,
303 }
304 }
305
306 pub fn is_balanced(&self) -> bool {
308 matches!(
309 self.interpretation,
310 BalanceInterpretation::Balanced | BalanceInterpretation::Acceptable
311 )
312 }
313
314 pub fn needs_refactoring(&self) -> bool {
316 matches!(
317 self.interpretation,
318 BalanceInterpretation::NeedsRefactoring | BalanceInterpretation::Critical
319 )
320 }
321}
322
323#[derive(Debug, Clone)]
325pub struct IssueThresholds {
326 pub strong_coupling: f64,
328 pub far_distance: f64,
330 pub high_volatility: f64,
332 pub max_dependencies: usize,
334 pub max_dependents: usize,
336}
337
338impl Default for IssueThresholds {
339 fn default() -> Self {
340 Self {
341 strong_coupling: 0.75, far_distance: 0.5, high_volatility: 0.75, max_dependencies: 20, max_dependents: 30, }
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum CrateStability {
353 Fundamental,
355 Stable,
357 Infrastructure,
359 Normal,
361}
362
363pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
365 let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
367
368 match base_name {
369 "std" | "core" | "alloc" => CrateStability::Fundamental,
371
372 "serde" | "serde_json" | "serde_yaml" | "toml" | "thiserror" | "anyhow" | "log" | "chrono" | "time" | "uuid" | "regex" | "lazy_static" | "once_cell" | "bytes" | "memchr" | "itertools" | "derive_more" | "strum" => CrateStability::Stable,
384
385 "tokio" | "async-std" | "smol" | "async-trait" | "futures" | "futures-util" | "tracing" | "tracing-subscriber" | "tracing-opentelemetry" | "opentelemetry" | "opentelemetry-otlp" | "opentelemetry_sdk" |
392 "hyper" | "reqwest" | "http" | "tonic" | "prost" | "sqlx" | "diesel" | "sea-orm" | "clap" | "structopt" => CrateStability::Infrastructure,
397
398 _ => CrateStability::Normal,
400 }
401}
402
403pub fn should_skip_crate(crate_name: &str) -> bool {
405 matches!(
406 classify_crate_stability(crate_name),
407 CrateStability::Fundamental
408 )
409}
410
411pub fn should_reduce_severity(crate_name: &str) -> bool {
413 matches!(
414 classify_crate_stability(crate_name),
415 CrateStability::Stable | CrateStability::Infrastructure
416 )
417}
418
419pub fn is_external_crate(target: &str, source: &str) -> bool {
422 let target_prefix = target.split("::").next().unwrap_or(target);
427 let source_prefix = source.split("::").next().unwrap_or(source);
428
429 if target_prefix == source_prefix {
431 return false;
432 }
433
434 let stability = classify_crate_stability(target);
437 matches!(
438 stability,
439 CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
440 )
441}
442
443pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
445 identify_issues_with_thresholds(coupling, &IssueThresholds::default())
446}
447
448pub fn identify_issues_with_thresholds(
450 coupling: &CouplingMetrics,
451 _thresholds: &IssueThresholds,
452) -> Vec<CouplingIssue> {
453 let mut issues = Vec::new();
454
455 if coupling.distance == Distance::DifferentCrate {
458 return issues;
459 }
460
461 let balance = BalanceScore::calculate(coupling);
462
463 if coupling.strength == IntegrationStrength::Intrusive
466 && coupling.distance == Distance::DifferentModule
467 {
468 issues.push(CouplingIssue {
469 issue_type: IssueType::GlobalComplexity,
470 severity: Severity::Medium, source: coupling.source.clone(),
472 target: coupling.target.clone(),
473 description: format!(
474 "Intrusive coupling to {} across module boundary",
475 coupling.target,
476 ),
477 refactoring: RefactoringAction::IntroduceTrait {
478 suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
479 methods: vec!["// Extract required methods".to_string()],
480 },
481 balance_score: balance.score,
482 });
483 }
484
485 if coupling.strength == IntegrationStrength::Intrusive
488 && coupling.volatility == Volatility::High
489 {
490 issues.push(CouplingIssue {
491 issue_type: IssueType::CascadingChangeRisk,
492 severity: Severity::High,
493 source: coupling.source.clone(),
494 target: coupling.target.clone(),
495 description: format!(
496 "Intrusive coupling to frequently-changed component {}",
497 coupling.target,
498 ),
499 refactoring: RefactoringAction::StabilizeInterface {
500 interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
501 },
502 balance_score: balance.score,
503 });
504 }
505
506 if coupling.strength == IntegrationStrength::Intrusive
509 && coupling.distance == Distance::DifferentModule
510 && balance.score < 0.5
511 {
512 if !issues
514 .iter()
515 .any(|i| i.issue_type == IssueType::GlobalComplexity)
516 {
517 issues.push(CouplingIssue {
518 issue_type: IssueType::InappropriateIntimacy,
519 severity: Severity::Medium,
520 source: coupling.source.clone(),
521 target: coupling.target.clone(),
522 description: format!(
523 "Direct internal access to {} across module boundary",
524 coupling.target,
525 ),
526 refactoring: RefactoringAction::IntroduceTrait {
527 suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
528 methods: vec!["// Expose only necessary operations".to_string()],
529 },
530 balance_score: balance.score,
531 });
532 }
533 }
534
535 issues
540}
541
542pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
544 analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
545}
546
547pub fn analyze_project_balance_with_thresholds(
549 metrics: &ProjectMetrics,
550 thresholds: &IssueThresholds,
551) -> ProjectBalanceReport {
552 let thresholds = thresholds.clone();
553 let mut all_issues = Vec::new();
554 let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
555 let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
556
557 for coupling in &metrics.couplings {
560 let score = BalanceScore::calculate(coupling);
561 all_balance_scores.push(score.clone());
562
563 if coupling.distance != Distance::DifferentCrate {
565 internal_balance_scores.push(score);
566 let issues = identify_issues_with_thresholds(coupling, &thresholds);
567 all_issues.extend(issues);
568 }
569 }
570
571 let module_issues = analyze_module_coupling(metrics, &thresholds);
573 all_issues.extend(module_issues);
574
575 all_issues.sort_by(|a, b| {
577 b.severity
578 .cmp(&a.severity)
579 .then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
580 });
581
582 let total_couplings = metrics.couplings.len();
584 let internal_couplings = internal_balance_scores.len();
585
586 let balanced_count = internal_balance_scores
587 .iter()
588 .filter(|s| s.is_balanced())
589 .count();
590 let needs_review = internal_balance_scores
591 .iter()
592 .filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
593 .count();
594 let needs_refactoring = internal_balance_scores
595 .iter()
596 .filter(|s| s.needs_refactoring())
597 .count();
598
599 let average_score = if internal_balance_scores.is_empty() {
601 1.0 } else {
603 internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
604 / internal_balance_scores.len() as f64
605 };
606
607 let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
609 for issue in &all_issues {
610 *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
611 }
612
613 let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
615 for issue in &all_issues {
616 *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
617 }
618
619 let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
621
622 ProjectBalanceReport {
623 total_couplings,
624 balanced_count,
625 needs_review,
626 needs_refactoring,
627 average_score,
628 health_grade,
629 issues_by_severity,
630 issues_by_type,
631 issues: all_issues,
632 top_priorities: Vec::new(), }
634 .with_top_priorities(5) }
636
637fn analyze_module_coupling(
639 metrics: &ProjectMetrics,
640 thresholds: &IssueThresholds,
641) -> Vec<CouplingIssue> {
642 let mut issues = Vec::new();
643
644 let mut efferent: HashMap<&str, usize> = HashMap::new();
647 let mut afferent: HashMap<&str, usize> = HashMap::new();
648
649 for coupling in &metrics.couplings {
650 if coupling.distance == Distance::DifferentCrate {
652 continue;
653 }
654
655 *efferent.entry(&coupling.source).or_insert(0) += 1;
656 *afferent.entry(&coupling.target).or_insert(0) += 1;
657 }
658
659 for (module, count) in &efferent {
661 if *count > thresholds.max_dependencies {
662 issues.push(CouplingIssue {
663 issue_type: IssueType::HighEfferentCoupling,
664 severity: if *count > thresholds.max_dependencies * 2 {
665 Severity::High
666 } else {
667 Severity::Medium
668 },
669 source: module.to_string(),
670 target: format!("{} dependencies", count),
671 description: format!(
672 "Module {} depends on {} other components (threshold: {})",
673 module, count, thresholds.max_dependencies
674 ),
675 refactoring: RefactoringAction::SplitModule {
676 suggested_modules: vec![
677 format!("{}_core", module),
678 format!("{}_integration", module),
679 ],
680 },
681 balance_score: 1.0
682 - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
683 });
684 }
685 }
686
687 for (module, count) in &afferent {
690 if *count > thresholds.max_dependents {
691 issues.push(CouplingIssue {
692 issue_type: IssueType::HighAfferentCoupling,
693 severity: if *count > thresholds.max_dependents * 2 {
694 Severity::High
695 } else {
696 Severity::Medium
697 },
698 source: format!("{} dependents", count),
699 target: module.to_string(),
700 description: format!(
701 "Module {} is depended on by {} other components (threshold: {})",
702 module, count, thresholds.max_dependents
703 ),
704 refactoring: RefactoringAction::IntroduceTrait {
705 suggested_name: format!("{}Interface", extract_type_name(module)),
706 methods: vec!["// Define stable public API".to_string()],
707 },
708 balance_score: 1.0
709 - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
710 });
711 }
712 }
713
714 issues
715}
716
717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719pub enum HealthGrade {
720 A, B, C, D, F, }
726
727impl std::fmt::Display for HealthGrade {
728 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729 match self {
730 HealthGrade::A => write!(f, "A (Excellent)"),
731 HealthGrade::B => write!(f, "B (Good)"),
732 HealthGrade::C => write!(f, "C (Acceptable)"),
733 HealthGrade::D => write!(f, "D (Needs Improvement)"),
734 HealthGrade::F => write!(f, "F (Critical Issues)"),
735 }
736 }
737}
738
739fn calculate_health_grade(
747 issues_by_severity: &HashMap<Severity, usize>,
748 internal_couplings: usize,
749) -> HealthGrade {
750 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
751 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
752 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
753
754 if internal_couplings == 0 {
756 return HealthGrade::B;
757 }
758
759 if critical > 3 {
761 return HealthGrade::F;
762 }
763
764 let high_density = high as f64 / internal_couplings as f64;
766 let medium_density = medium as f64 / internal_couplings as f64;
767 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
768
769 if critical > 0 || high_density > 0.05 {
771 return HealthGrade::D;
772 }
773
774 if high > 0 || medium_density > 0.25 {
777 return HealthGrade::C;
778 }
779
780 if medium_density > 0.05 || total_issue_density > 0.10 {
782 return HealthGrade::B;
783 }
784
785 if high == 0 && medium_density <= 0.05 && internal_couplings >= 10 {
788 return HealthGrade::A;
789 }
790
791 HealthGrade::B
793}
794
795#[derive(Debug)]
797pub struct ProjectBalanceReport {
798 pub total_couplings: usize,
799 pub balanced_count: usize,
800 pub needs_review: usize,
801 pub needs_refactoring: usize,
802 pub average_score: f64,
803 pub health_grade: HealthGrade,
804 pub issues_by_severity: HashMap<Severity, usize>,
805 pub issues_by_type: HashMap<IssueType, usize>,
806 pub issues: Vec<CouplingIssue>,
807 pub top_priorities: Vec<CouplingIssue>,
808}
809
810impl ProjectBalanceReport {
811 fn with_top_priorities(mut self, n: usize) -> Self {
813 self.top_priorities = self.issues.iter().take(n).cloned().collect();
814 self
815 }
816
817 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
819 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
820 for issue in &self.issues {
821 grouped.entry(issue.issue_type).or_default().push(issue);
822 }
823 grouped
824 }
825}
826
827pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
832 let internal_scores: Vec<f64> = metrics
834 .couplings
835 .iter()
836 .filter(|c| c.distance != Distance::DifferentCrate)
837 .map(|c| BalanceScore::calculate(c).score)
838 .collect();
839
840 if internal_scores.is_empty() {
841 return 1.0; }
843
844 internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
845}
846
847fn extract_type_name(path: &str) -> String {
850 path.split("::")
851 .last()
852 .unwrap_or(path)
853 .chars()
854 .enumerate()
855 .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
856 .collect()
857}
858
859pub fn strength_label(strength: IntegrationStrength) -> &'static str {
861 match strength {
862 IntegrationStrength::Intrusive => "Intrusive",
863 IntegrationStrength::Functional => "Functional",
864 IntegrationStrength::Model => "Model",
865 IntegrationStrength::Contract => "Contract",
866 }
867}
868
869pub fn distance_label(distance: Distance) -> &'static str {
871 match distance {
872 Distance::SameFunction => "same function",
873 Distance::SameModule => "same module",
874 Distance::DifferentModule => "different module",
875 Distance::DifferentCrate => "external crate",
876 }
877}
878
879pub fn volatility_label(volatility: Volatility) -> &'static str {
881 match volatility {
882 Volatility::Low => "rarely",
883 Volatility::Medium => "sometimes",
884 Volatility::High => "frequently",
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 fn make_coupling(
893 strength: IntegrationStrength,
894 distance: Distance,
895 volatility: Volatility,
896 ) -> CouplingMetrics {
897 CouplingMetrics::new(
898 "source::module".to_string(),
899 "target::module".to_string(),
900 strength,
901 distance,
902 volatility,
903 )
904 }
905
906 #[test]
907 fn test_balance_ideal_close() {
908 let coupling = make_coupling(
910 IntegrationStrength::Intrusive,
911 Distance::SameModule,
912 Volatility::Low,
913 );
914 let score = BalanceScore::calculate(&coupling);
915 assert!(score.is_balanced(), "Score: {}", score.score);
916 }
917
918 #[test]
919 fn test_balance_ideal_far() {
920 let coupling = make_coupling(
922 IntegrationStrength::Contract,
923 Distance::DifferentCrate,
924 Volatility::Low,
925 );
926 let score = BalanceScore::calculate(&coupling);
927 assert!(score.is_balanced(), "Score: {}", score.score);
928 }
929
930 #[test]
931 fn test_balance_bad_global_complexity() {
932 let coupling = make_coupling(
934 IntegrationStrength::Intrusive,
935 Distance::DifferentCrate,
936 Volatility::Low,
937 );
938 let score = BalanceScore::calculate(&coupling);
939 assert!(
940 score.needs_refactoring(),
941 "Score: {}, should need refactoring",
942 score.score
943 );
944 }
945
946 #[test]
947 fn test_balance_bad_cascading() {
948 let coupling = make_coupling(
950 IntegrationStrength::Intrusive,
951 Distance::SameModule,
952 Volatility::High,
953 );
954 let score = BalanceScore::calculate(&coupling);
955 assert!(
956 !score.is_balanced(),
957 "Score: {}, should not be balanced due to volatility",
958 score.score
959 );
960 }
961
962 #[test]
963 fn test_identify_global_complexity() {
964 let coupling = make_coupling(
967 IntegrationStrength::Intrusive,
968 Distance::DifferentModule,
969 Volatility::Low,
970 );
971 let issues = identify_issues(&coupling);
972 assert!(
973 !issues.is_empty(),
974 "Should identify global complexity issue for internal cross-module coupling"
975 );
976 assert!(
977 issues
978 .iter()
979 .any(|i| i.issue_type == IssueType::GlobalComplexity)
980 );
981 }
982
983 #[test]
984 fn test_external_crates_are_skipped() {
985 let coupling = make_coupling(
987 IntegrationStrength::Intrusive,
988 Distance::DifferentCrate,
989 Volatility::Low,
990 );
991 let issues = identify_issues(&coupling);
992 assert!(
993 issues.is_empty(),
994 "External crate dependencies should be skipped"
995 );
996 }
997
998 #[test]
999 fn test_identify_cascading_change() {
1000 let coupling = make_coupling(
1002 IntegrationStrength::Intrusive,
1003 Distance::SameModule,
1004 Volatility::High,
1005 );
1006 let issues = identify_issues(&coupling);
1007 assert!(
1008 issues
1009 .iter()
1010 .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1011 "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_identify_inappropriate_intimacy() {
1017 let coupling = make_coupling(
1020 IntegrationStrength::Intrusive,
1021 Distance::DifferentModule,
1022 Volatility::Low,
1023 );
1024 let issues = identify_issues(&coupling);
1025 assert!(
1026 issues
1027 .iter()
1028 .any(|i| i.issue_type == IssueType::GlobalComplexity),
1029 "Intrusive + DifferentModule should detect GlobalComplexity"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_no_issues_for_balanced() {
1035 let coupling = make_coupling(
1037 IntegrationStrength::Model,
1038 Distance::DifferentModule,
1039 Volatility::Low,
1040 );
1041 let issues = identify_issues(&coupling);
1042 assert!(
1044 issues.is_empty(),
1045 "Model coupling should not generate issues"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_health_grade_calculation() {
1051 let mut issues = HashMap::new();
1052
1053 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::A);
1055
1056 assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1058
1059 issues.insert(Severity::High, 1);
1061 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1062
1063 issues.clear();
1065 issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1067
1068 issues.clear();
1070 issues.insert(Severity::Critical, 1);
1071 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1072
1073 issues.clear();
1075 issues.insert(Severity::Critical, 4);
1076 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1077
1078 issues.clear();
1080 issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1082
1083 issues.clear();
1085 issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1087 }
1088}