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
604 .iter()
605 .map(|s| s.score)
606 .sum::<f64>()
607 / internal_balance_scores.len() as f64
608 };
609
610 let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
612 for issue in &all_issues {
613 *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
614 }
615
616 let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
618 for issue in &all_issues {
619 *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
620 }
621
622 let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
624
625 ProjectBalanceReport {
626 total_couplings,
627 balanced_count,
628 needs_review,
629 needs_refactoring,
630 average_score,
631 health_grade,
632 issues_by_severity,
633 issues_by_type,
634 issues: all_issues,
635 top_priorities: Vec::new(), }
637 .with_top_priorities(5) }
639
640fn analyze_module_coupling(
642 metrics: &ProjectMetrics,
643 thresholds: &IssueThresholds,
644) -> Vec<CouplingIssue> {
645 let mut issues = Vec::new();
646
647 let mut efferent: HashMap<&str, usize> = HashMap::new();
650 let mut afferent: HashMap<&str, usize> = HashMap::new();
651
652 for coupling in &metrics.couplings {
653 if coupling.distance == Distance::DifferentCrate {
655 continue;
656 }
657
658 *efferent.entry(&coupling.source).or_insert(0) += 1;
659 *afferent.entry(&coupling.target).or_insert(0) += 1;
660 }
661
662 for (module, count) in &efferent {
664 if *count > thresholds.max_dependencies {
665 issues.push(CouplingIssue {
666 issue_type: IssueType::HighEfferentCoupling,
667 severity: if *count > thresholds.max_dependencies * 2 {
668 Severity::High
669 } else {
670 Severity::Medium
671 },
672 source: module.to_string(),
673 target: format!("{} dependencies", count),
674 description: format!(
675 "Module {} depends on {} other components (threshold: {})",
676 module, count, thresholds.max_dependencies
677 ),
678 refactoring: RefactoringAction::SplitModule {
679 suggested_modules: vec![
680 format!("{}_core", module),
681 format!("{}_integration", module),
682 ],
683 },
684 balance_score: 1.0
685 - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
686 });
687 }
688 }
689
690 for (module, count) in &afferent {
693 if *count > thresholds.max_dependents {
694 issues.push(CouplingIssue {
695 issue_type: IssueType::HighAfferentCoupling,
696 severity: if *count > thresholds.max_dependents * 2 {
697 Severity::High
698 } else {
699 Severity::Medium
700 },
701 source: format!("{} dependents", count),
702 target: module.to_string(),
703 description: format!(
704 "Module {} is depended on by {} other components (threshold: {})",
705 module, count, thresholds.max_dependents
706 ),
707 refactoring: RefactoringAction::IntroduceTrait {
708 suggested_name: format!("{}Interface", extract_type_name(module)),
709 methods: vec!["// Define stable public API".to_string()],
710 },
711 balance_score: 1.0
712 - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
713 });
714 }
715 }
716
717 issues
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
722pub enum HealthGrade {
723 A, B, C, D, F, }
729
730impl std::fmt::Display for HealthGrade {
731 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
732 match self {
733 HealthGrade::A => write!(f, "A (Excellent)"),
734 HealthGrade::B => write!(f, "B (Good)"),
735 HealthGrade::C => write!(f, "C (Acceptable)"),
736 HealthGrade::D => write!(f, "D (Needs Improvement)"),
737 HealthGrade::F => write!(f, "F (Critical Issues)"),
738 }
739 }
740}
741
742fn calculate_health_grade(
750 issues_by_severity: &HashMap<Severity, usize>,
751 internal_couplings: usize,
752) -> HealthGrade {
753 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
754 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
755 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
756
757 if internal_couplings == 0 {
759 return HealthGrade::B;
760 }
761
762 if critical > 3 {
764 return HealthGrade::F;
765 }
766
767 let high_density = high as f64 / internal_couplings as f64;
769 let medium_density = medium as f64 / internal_couplings as f64;
770 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
771
772 if critical > 0 || high_density > 0.05 {
774 return HealthGrade::D;
775 }
776
777 if high > 0 || medium_density > 0.25 {
780 return HealthGrade::C;
781 }
782
783 if medium_density > 0.05 || total_issue_density > 0.10 {
785 return HealthGrade::B;
786 }
787
788 if high == 0 && medium_density <= 0.05 && internal_couplings >= 10 {
791 return HealthGrade::A;
792 }
793
794 HealthGrade::B
796}
797
798#[derive(Debug)]
800pub struct ProjectBalanceReport {
801 pub total_couplings: usize,
802 pub balanced_count: usize,
803 pub needs_review: usize,
804 pub needs_refactoring: usize,
805 pub average_score: f64,
806 pub health_grade: HealthGrade,
807 pub issues_by_severity: HashMap<Severity, usize>,
808 pub issues_by_type: HashMap<IssueType, usize>,
809 pub issues: Vec<CouplingIssue>,
810 pub top_priorities: Vec<CouplingIssue>,
811}
812
813impl ProjectBalanceReport {
814 fn with_top_priorities(mut self, n: usize) -> Self {
816 self.top_priorities = self.issues.iter().take(n).cloned().collect();
817 self
818 }
819
820 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
822 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
823 for issue in &self.issues {
824 grouped.entry(issue.issue_type).or_default().push(issue);
825 }
826 grouped
827 }
828}
829
830pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
835 let internal_scores: Vec<f64> = metrics
837 .couplings
838 .iter()
839 .filter(|c| c.distance != Distance::DifferentCrate)
840 .map(|c| BalanceScore::calculate(c).score)
841 .collect();
842
843 if internal_scores.is_empty() {
844 return 1.0; }
846
847 internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
848}
849
850fn extract_type_name(path: &str) -> String {
853 path.split("::")
854 .last()
855 .unwrap_or(path)
856 .chars()
857 .enumerate()
858 .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
859 .collect()
860}
861
862pub fn strength_label(strength: IntegrationStrength) -> &'static str {
864 match strength {
865 IntegrationStrength::Intrusive => "Intrusive",
866 IntegrationStrength::Functional => "Functional",
867 IntegrationStrength::Model => "Model",
868 IntegrationStrength::Contract => "Contract",
869 }
870}
871
872pub fn distance_label(distance: Distance) -> &'static str {
874 match distance {
875 Distance::SameFunction => "same function",
876 Distance::SameModule => "same module",
877 Distance::DifferentModule => "different module",
878 Distance::DifferentCrate => "external crate",
879 }
880}
881
882pub fn volatility_label(volatility: Volatility) -> &'static str {
884 match volatility {
885 Volatility::Low => "rarely",
886 Volatility::Medium => "sometimes",
887 Volatility::High => "frequently",
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 fn make_coupling(
896 strength: IntegrationStrength,
897 distance: Distance,
898 volatility: Volatility,
899 ) -> CouplingMetrics {
900 CouplingMetrics::new(
901 "source::module".to_string(),
902 "target::module".to_string(),
903 strength,
904 distance,
905 volatility,
906 )
907 }
908
909 #[test]
910 fn test_balance_ideal_close() {
911 let coupling = make_coupling(
913 IntegrationStrength::Intrusive,
914 Distance::SameModule,
915 Volatility::Low,
916 );
917 let score = BalanceScore::calculate(&coupling);
918 assert!(score.is_balanced(), "Score: {}", score.score);
919 }
920
921 #[test]
922 fn test_balance_ideal_far() {
923 let coupling = make_coupling(
925 IntegrationStrength::Contract,
926 Distance::DifferentCrate,
927 Volatility::Low,
928 );
929 let score = BalanceScore::calculate(&coupling);
930 assert!(score.is_balanced(), "Score: {}", score.score);
931 }
932
933 #[test]
934 fn test_balance_bad_global_complexity() {
935 let coupling = make_coupling(
937 IntegrationStrength::Intrusive,
938 Distance::DifferentCrate,
939 Volatility::Low,
940 );
941 let score = BalanceScore::calculate(&coupling);
942 assert!(
943 score.needs_refactoring(),
944 "Score: {}, should need refactoring",
945 score.score
946 );
947 }
948
949 #[test]
950 fn test_balance_bad_cascading() {
951 let coupling = make_coupling(
953 IntegrationStrength::Intrusive,
954 Distance::SameModule,
955 Volatility::High,
956 );
957 let score = BalanceScore::calculate(&coupling);
958 assert!(
959 !score.is_balanced(),
960 "Score: {}, should not be balanced due to volatility",
961 score.score
962 );
963 }
964
965 #[test]
966 fn test_identify_global_complexity() {
967 let coupling = make_coupling(
970 IntegrationStrength::Intrusive,
971 Distance::DifferentModule,
972 Volatility::Low,
973 );
974 let issues = identify_issues(&coupling);
975 assert!(
976 !issues.is_empty(),
977 "Should identify global complexity issue for internal cross-module coupling"
978 );
979 assert!(
980 issues
981 .iter()
982 .any(|i| i.issue_type == IssueType::GlobalComplexity)
983 );
984 }
985
986 #[test]
987 fn test_external_crates_are_skipped() {
988 let coupling = make_coupling(
990 IntegrationStrength::Intrusive,
991 Distance::DifferentCrate,
992 Volatility::Low,
993 );
994 let issues = identify_issues(&coupling);
995 assert!(
996 issues.is_empty(),
997 "External crate dependencies should be skipped"
998 );
999 }
1000
1001 #[test]
1002 fn test_identify_cascading_change() {
1003 let coupling = make_coupling(
1005 IntegrationStrength::Intrusive,
1006 Distance::SameModule,
1007 Volatility::High,
1008 );
1009 let issues = identify_issues(&coupling);
1010 assert!(
1011 issues
1012 .iter()
1013 .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1014 "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_identify_inappropriate_intimacy() {
1020 let coupling = make_coupling(
1023 IntegrationStrength::Intrusive,
1024 Distance::DifferentModule,
1025 Volatility::Low,
1026 );
1027 let issues = identify_issues(&coupling);
1028 assert!(
1029 issues
1030 .iter()
1031 .any(|i| i.issue_type == IssueType::GlobalComplexity),
1032 "Intrusive + DifferentModule should detect GlobalComplexity"
1033 );
1034 }
1035
1036 #[test]
1037 fn test_no_issues_for_balanced() {
1038 let coupling = make_coupling(
1040 IntegrationStrength::Model,
1041 Distance::DifferentModule,
1042 Volatility::Low,
1043 );
1044 let issues = identify_issues(&coupling);
1045 assert!(
1047 issues.is_empty(),
1048 "Model coupling should not generate issues"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_health_grade_calculation() {
1054 let mut issues = HashMap::new();
1055
1056 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::A);
1058
1059 assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1061
1062 issues.insert(Severity::High, 1);
1064 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1065
1066 issues.clear();
1068 issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1070
1071 issues.clear();
1073 issues.insert(Severity::Critical, 1);
1074 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1075
1076 issues.clear();
1078 issues.insert(Severity::Critical, 4);
1079 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1080
1081 issues.clear();
1083 issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1085
1086 issues.clear();
1088 issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1090 }
1091}