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 GodModule,
69 PublicFieldExposure,
71 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 IssueType::ShallowModule => write!(f, "Shallow Module"),
87 IssueType::PassThroughMethod => write!(f, "Pass-Through Method"),
88 IssueType::HighCognitiveLoad => write!(f, "High Cognitive Load"),
89 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 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 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 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#[derive(Debug, Clone)]
148pub struct CouplingIssue {
149 pub issue_type: IssueType,
151 pub severity: Severity,
153 pub source: String,
155 pub target: String,
157 pub description: String,
159 pub refactoring: RefactoringAction,
161 pub balance_score: f64,
163}
164
165#[derive(Debug, Clone)]
167pub enum RefactoringAction {
168 IntroduceTrait {
170 suggested_name: String,
171 methods: Vec<String>,
172 },
173 MoveCloser { target_location: String },
175 ExtractAdapter {
177 adapter_name: String,
178 purpose: String,
179 },
180 SplitModule { suggested_modules: Vec<String> },
182 SimplifyAbstraction { direct_usage: String },
184 BreakCycle { suggested_direction: String },
186 StabilizeInterface { interface_name: String },
188 General { action: String },
190 AddGetters { fields: Vec<String> },
192 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#[derive(Debug, Clone)]
258pub struct BalanceScore {
259 pub coupling: CouplingMetrics,
261 pub score: f64,
263 pub alignment: f64,
265 pub volatility_impact: f64,
267 pub interpretation: BalanceInterpretation,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum BalanceInterpretation {
274 Balanced,
276 Acceptable,
278 NeedsReview,
280 NeedsRefactoring,
282 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 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 let alignment = 1.0 - (strength - (1.0 - distance)).abs();
320
321 let volatility_penalty = volatility * strength;
324 let volatility_impact = 1.0 - volatility_penalty;
325
326 let score = alignment * volatility_impact;
329
330 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 pub fn is_balanced(&self) -> bool {
350 matches!(
351 self.interpretation,
352 BalanceInterpretation::Balanced | BalanceInterpretation::Acceptable
353 )
354 }
355
356 pub fn needs_refactoring(&self) -> bool {
358 matches!(
359 self.interpretation,
360 BalanceInterpretation::NeedsRefactoring | BalanceInterpretation::Critical
361 )
362 }
363}
364
365#[derive(Debug, Clone)]
367pub struct IssueThresholds {
368 pub strong_coupling: f64,
370 pub far_distance: f64,
372 pub high_volatility: f64,
374 pub max_dependencies: usize,
376 pub max_dependents: usize,
378 pub max_functions: usize,
380 pub max_types: usize,
382 pub max_impls: usize,
384 pub min_primitive_params: usize,
386 pub strict_mode: bool,
388 pub japanese: bool,
390}
391
392impl Default for IssueThresholds {
393 fn default() -> Self {
394 Self {
395 strong_coupling: 0.75, far_distance: 0.5, high_volatility: 0.75, max_dependencies: 20, max_dependents: 30, max_functions: 30, max_types: 15, max_impls: 20, min_primitive_params: 3, strict_mode: true, japanese: false, }
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub enum CrateStability {
413 Fundamental,
415 Stable,
417 Infrastructure,
419 Normal,
421}
422
423pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
425 let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
427
428 match base_name {
429 "std" | "core" | "alloc" => CrateStability::Fundamental,
431
432 "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,
444
445 "tokio" | "async-std" | "smol" | "async-trait" | "futures" | "futures-util" | "tracing" | "tracing-subscriber" | "tracing-opentelemetry" | "opentelemetry" | "opentelemetry-otlp" | "opentelemetry_sdk" |
452 "hyper" | "reqwest" | "http" | "tonic" | "prost" | "sqlx" | "diesel" | "sea-orm" | "clap" | "structopt" => CrateStability::Infrastructure,
457
458 _ => CrateStability::Normal,
460 }
461}
462
463pub fn should_skip_crate(crate_name: &str) -> bool {
465 matches!(
466 classify_crate_stability(crate_name),
467 CrateStability::Fundamental
468 )
469}
470
471pub fn should_reduce_severity(crate_name: &str) -> bool {
473 matches!(
474 classify_crate_stability(crate_name),
475 CrateStability::Stable | CrateStability::Infrastructure
476 )
477}
478
479pub fn is_external_crate(target: &str, source: &str) -> bool {
482 let target_prefix = target.split("::").next().unwrap_or(target);
487 let source_prefix = source.split("::").next().unwrap_or(source);
488
489 if target_prefix == source_prefix {
491 return false;
492 }
493
494 let stability = classify_crate_stability(target);
497 matches!(
498 stability,
499 CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
500 )
501}
502
503pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
505 identify_issues_with_thresholds(coupling, &IssueThresholds::default())
506}
507
508pub fn identify_issues_with_thresholds(
510 coupling: &CouplingMetrics,
511 _thresholds: &IssueThresholds,
512) -> Vec<CouplingIssue> {
513 let mut issues = Vec::new();
514
515 if coupling.distance == Distance::DifferentCrate {
518 return issues;
519 }
520
521 let balance = BalanceScore::calculate(coupling);
522
523 if coupling.strength == IntegrationStrength::Intrusive
526 && coupling.distance == Distance::DifferentModule
527 {
528 issues.push(CouplingIssue {
529 issue_type: IssueType::GlobalComplexity,
530 severity: Severity::Medium, source: coupling.source.clone(),
532 target: coupling.target.clone(),
533 description: format!(
534 "Intrusive coupling to {} across module boundary",
535 coupling.target,
536 ),
537 refactoring: RefactoringAction::IntroduceTrait {
538 suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
539 methods: vec!["// Extract required methods".to_string()],
540 },
541 balance_score: balance.score,
542 });
543 }
544
545 if coupling.strength == IntegrationStrength::Intrusive
548 && coupling.volatility == Volatility::High
549 {
550 issues.push(CouplingIssue {
551 issue_type: IssueType::CascadingChangeRisk,
552 severity: Severity::High,
553 source: coupling.source.clone(),
554 target: coupling.target.clone(),
555 description: format!(
556 "Intrusive coupling to frequently-changed component {}",
557 coupling.target,
558 ),
559 refactoring: RefactoringAction::StabilizeInterface {
560 interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
561 },
562 balance_score: balance.score,
563 });
564 }
565
566 if coupling.strength == IntegrationStrength::Intrusive
569 && coupling.distance == Distance::DifferentModule
570 && balance.score < 0.5
571 {
572 if !issues
574 .iter()
575 .any(|i| i.issue_type == IssueType::GlobalComplexity)
576 {
577 issues.push(CouplingIssue {
578 issue_type: IssueType::InappropriateIntimacy,
579 severity: Severity::Medium,
580 source: coupling.source.clone(),
581 target: coupling.target.clone(),
582 description: format!(
583 "Direct internal access to {} across module boundary",
584 coupling.target,
585 ),
586 refactoring: RefactoringAction::IntroduceTrait {
587 suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
588 methods: vec!["// Expose only necessary operations".to_string()],
589 },
590 balance_score: balance.score,
591 });
592 }
593 }
594
595 issues
600}
601
602pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
604 analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
605}
606
607pub fn analyze_project_balance_with_thresholds(
609 metrics: &ProjectMetrics,
610 thresholds: &IssueThresholds,
611) -> ProjectBalanceReport {
612 let thresholds = thresholds.clone();
613 let mut all_issues = Vec::new();
614 let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
615 let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
616
617 for coupling in &metrics.couplings {
620 let score = BalanceScore::calculate(coupling);
621 all_balance_scores.push(score.clone());
622
623 if coupling.distance != Distance::DifferentCrate {
625 internal_balance_scores.push(score);
626 let issues = identify_issues_with_thresholds(coupling, &thresholds);
627 all_issues.extend(issues);
628 }
629 }
630
631 let module_issues = analyze_module_coupling(metrics, &thresholds);
633 all_issues.extend(module_issues);
634
635 let rust_issues = analyze_rust_patterns(metrics, &thresholds);
637 all_issues.extend(rust_issues);
638
639 if thresholds.strict_mode {
641 all_issues.retain(|issue| issue.severity >= Severity::Medium);
642 }
643
644 all_issues.sort_by(|a, b| {
646 b.severity
647 .cmp(&a.severity)
648 .then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
649 });
650
651 let total_couplings = metrics.couplings.len();
653 let internal_couplings = internal_balance_scores.len();
654
655 let balanced_count = internal_balance_scores
656 .iter()
657 .filter(|s| s.is_balanced())
658 .count();
659 let needs_review = internal_balance_scores
660 .iter()
661 .filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
662 .count();
663 let needs_refactoring = internal_balance_scores
664 .iter()
665 .filter(|s| s.needs_refactoring())
666 .count();
667
668 let average_score = if internal_balance_scores.is_empty() {
670 1.0 } else {
672 internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
673 / internal_balance_scores.len() as f64
674 };
675
676 let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
678 for issue in &all_issues {
679 *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
680 }
681
682 let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
684 for issue in &all_issues {
685 *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
686 }
687
688 let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
690
691 ProjectBalanceReport {
692 total_couplings,
693 balanced_count,
694 needs_review,
695 needs_refactoring,
696 average_score,
697 health_grade,
698 issues_by_severity,
699 issues_by_type,
700 issues: all_issues,
701 top_priorities: Vec::new(), }
703 .with_top_priorities(5) }
705
706fn analyze_module_coupling(
708 metrics: &ProjectMetrics,
709 thresholds: &IssueThresholds,
710) -> Vec<CouplingIssue> {
711 let mut issues = Vec::new();
712
713 let mut efferent: HashMap<&str, usize> = HashMap::new();
716 let mut afferent: HashMap<&str, usize> = HashMap::new();
717
718 for coupling in &metrics.couplings {
719 if coupling.distance == Distance::DifferentCrate {
721 continue;
722 }
723
724 *efferent.entry(&coupling.source).or_insert(0) += 1;
725 *afferent.entry(&coupling.target).or_insert(0) += 1;
726 }
727
728 for (module, count) in &efferent {
730 if *count > thresholds.max_dependencies {
731 issues.push(CouplingIssue {
732 issue_type: IssueType::HighEfferentCoupling,
733 severity: if *count > thresholds.max_dependencies * 2 {
734 Severity::High
735 } else {
736 Severity::Medium
737 },
738 source: module.to_string(),
739 target: format!("{} dependencies", count),
740 description: format!(
741 "Module {} depends on {} other components (threshold: {})",
742 module, count, thresholds.max_dependencies
743 ),
744 refactoring: RefactoringAction::SplitModule {
745 suggested_modules: vec![
746 format!("{}_core", module),
747 format!("{}_integration", module),
748 ],
749 },
750 balance_score: 1.0
751 - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
752 });
753 }
754 }
755
756 for (module, count) in &afferent {
759 if *count > thresholds.max_dependents {
760 issues.push(CouplingIssue {
761 issue_type: IssueType::HighAfferentCoupling,
762 severity: if *count > thresholds.max_dependents * 2 {
763 Severity::High
764 } else {
765 Severity::Medium
766 },
767 source: format!("{} dependents", count),
768 target: module.to_string(),
769 description: format!(
770 "Module {} is depended on by {} other components (threshold: {})",
771 module, count, thresholds.max_dependents
772 ),
773 refactoring: RefactoringAction::IntroduceTrait {
774 suggested_name: format!("{}Interface", extract_type_name(module)),
775 methods: vec!["// Define stable public API".to_string()],
776 },
777 balance_score: 1.0
778 - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
779 });
780 }
781 }
782
783 issues
784}
785
786fn analyze_rust_patterns(
788 metrics: &ProjectMetrics,
789 thresholds: &IssueThresholds,
790) -> Vec<CouplingIssue> {
791 let mut issues = Vec::new();
792
793 for (module_name, module) in &metrics.modules {
795 if module.is_god_module(
796 thresholds.max_functions,
797 thresholds.max_types,
798 thresholds.max_impls,
799 ) {
800 let func_count = module.function_count();
801 let type_count = module.type_definitions.len();
802 let impl_count = module.trait_impl_count + module.inherent_impl_count;
803
804 issues.push(CouplingIssue {
805 issue_type: IssueType::GodModule,
806 severity: if func_count > thresholds.max_functions * 2
807 || type_count > thresholds.max_types * 2
808 {
809 Severity::High
810 } else {
811 Severity::Medium
812 },
813 source: module_name.clone(),
814 target: format!(
815 "{} functions, {} types, {} impls",
816 func_count, type_count, impl_count
817 ),
818 description: format!(
819 "Module {} has too many responsibilities (functions: {}/{}, types: {}/{}, impls: {}/{})",
820 module_name,
821 func_count, thresholds.max_functions,
822 type_count, thresholds.max_types,
823 impl_count, thresholds.max_impls,
824 ),
825 refactoring: RefactoringAction::SplitModule {
826 suggested_modules: vec![
827 format!("{}_core", module_name),
828 format!("{}_helpers", module_name),
829 ],
830 },
831 balance_score: 0.5,
832 });
833 }
834
835 for type_def in module.type_definitions.values() {
837 if type_def.public_field_count > 0
838 && !type_def.is_trait
839 && type_def.visibility == crate::metrics::Visibility::Public
840 {
841 issues.push(CouplingIssue {
842 issue_type: IssueType::PublicFieldExposure,
843 severity: Severity::Low,
844 source: format!("{}::{}", module_name, type_def.name),
845 target: format!("{} public fields", type_def.public_field_count),
846 description: format!(
847 "Type {} has {} public field(s). Consider using getter methods.",
848 type_def.name, type_def.public_field_count
849 ),
850 refactoring: RefactoringAction::AddGetters {
851 fields: vec!["// Add getter methods".to_string()],
852 },
853 balance_score: 0.7,
854 });
855 }
856 }
857
858 for func_def in module.function_definitions.values() {
860 if func_def.primitive_param_count >= thresholds.min_primitive_params
861 && func_def.param_count >= thresholds.min_primitive_params
862 {
863 let ratio = func_def.primitive_param_count as f64 / func_def.param_count as f64;
864 if ratio >= 0.6 {
865 issues.push(CouplingIssue {
866 issue_type: IssueType::PrimitiveObsession,
867 severity: Severity::Low,
868 source: format!("{}::{}", module_name, func_def.name),
869 target: format!(
870 "{}/{} primitive params",
871 func_def.primitive_param_count, func_def.param_count
872 ),
873 description: format!(
874 "Function {} has {} primitive parameters. Consider newtype pattern.",
875 func_def.name, func_def.primitive_param_count
876 ),
877 refactoring: RefactoringAction::IntroduceNewtype {
878 suggested_name: format!("{}Params", capitalize_first(&func_def.name)),
879 wrapped_type: "// Group related parameters".to_string(),
880 },
881 balance_score: 0.7,
882 });
883 }
884 }
885 }
886 }
887
888 issues
889}
890
891fn capitalize_first(s: &str) -> String {
893 let mut chars = s.chars();
894 match chars.next() {
895 None => String::new(),
896 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
897 }
898}
899
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
902pub enum HealthGrade {
903 A, B, C, D, F, }
909
910impl std::fmt::Display for HealthGrade {
911 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
912 match self {
913 HealthGrade::A => write!(f, "A (Excellent)"),
914 HealthGrade::B => write!(f, "B (Good)"),
915 HealthGrade::C => write!(f, "C (Acceptable)"),
916 HealthGrade::D => write!(f, "D (Needs Improvement)"),
917 HealthGrade::F => write!(f, "F (Critical Issues)"),
918 }
919 }
920}
921
922fn calculate_health_grade(
930 issues_by_severity: &HashMap<Severity, usize>,
931 internal_couplings: usize,
932) -> HealthGrade {
933 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
934 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
935 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
936
937 if internal_couplings == 0 {
939 return HealthGrade::B;
940 }
941
942 if critical > 3 {
944 return HealthGrade::F;
945 }
946
947 let high_density = high as f64 / internal_couplings as f64;
949 let medium_density = medium as f64 / internal_couplings as f64;
950 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
951
952 if critical > 0 || high_density > 0.05 {
954 return HealthGrade::D;
955 }
956
957 if high > 0 || medium_density > 0.25 {
960 return HealthGrade::C;
961 }
962
963 if medium_density > 0.05 || total_issue_density > 0.10 {
965 return HealthGrade::B;
966 }
967
968 if high == 0 && medium_density <= 0.05 && internal_couplings >= 10 {
971 return HealthGrade::A;
972 }
973
974 HealthGrade::B
976}
977
978#[derive(Debug)]
980pub struct ProjectBalanceReport {
981 pub total_couplings: usize,
982 pub balanced_count: usize,
983 pub needs_review: usize,
984 pub needs_refactoring: usize,
985 pub average_score: f64,
986 pub health_grade: HealthGrade,
987 pub issues_by_severity: HashMap<Severity, usize>,
988 pub issues_by_type: HashMap<IssueType, usize>,
989 pub issues: Vec<CouplingIssue>,
990 pub top_priorities: Vec<CouplingIssue>,
991}
992
993impl ProjectBalanceReport {
994 fn with_top_priorities(mut self, n: usize) -> Self {
996 self.top_priorities = self.issues.iter().take(n).cloned().collect();
997 self
998 }
999
1000 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
1002 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
1003 for issue in &self.issues {
1004 grouped.entry(issue.issue_type).or_default().push(issue);
1005 }
1006 grouped
1007 }
1008}
1009
1010pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
1015 let internal_scores: Vec<f64> = metrics
1017 .couplings
1018 .iter()
1019 .filter(|c| c.distance != Distance::DifferentCrate)
1020 .map(|c| BalanceScore::calculate(c).score)
1021 .collect();
1022
1023 if internal_scores.is_empty() {
1024 return 1.0; }
1026
1027 internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
1028}
1029
1030fn extract_type_name(path: &str) -> String {
1033 path.split("::")
1034 .last()
1035 .unwrap_or(path)
1036 .chars()
1037 .enumerate()
1038 .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
1039 .collect()
1040}
1041
1042pub fn strength_label(strength: IntegrationStrength) -> &'static str {
1044 match strength {
1045 IntegrationStrength::Intrusive => "Intrusive",
1046 IntegrationStrength::Functional => "Functional",
1047 IntegrationStrength::Model => "Model",
1048 IntegrationStrength::Contract => "Contract",
1049 }
1050}
1051
1052pub fn distance_label(distance: Distance) -> &'static str {
1054 match distance {
1055 Distance::SameFunction => "same function",
1056 Distance::SameModule => "same module",
1057 Distance::DifferentModule => "different module",
1058 Distance::DifferentCrate => "external crate",
1059 }
1060}
1061
1062pub fn volatility_label(volatility: Volatility) -> &'static str {
1064 match volatility {
1065 Volatility::Low => "rarely",
1066 Volatility::Medium => "sometimes",
1067 Volatility::High => "frequently",
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074
1075 fn make_coupling(
1076 strength: IntegrationStrength,
1077 distance: Distance,
1078 volatility: Volatility,
1079 ) -> CouplingMetrics {
1080 CouplingMetrics::new(
1081 "source::module".to_string(),
1082 "target::module".to_string(),
1083 strength,
1084 distance,
1085 volatility,
1086 )
1087 }
1088
1089 #[test]
1090 fn test_balance_ideal_close() {
1091 let coupling = make_coupling(
1093 IntegrationStrength::Intrusive,
1094 Distance::SameModule,
1095 Volatility::Low,
1096 );
1097 let score = BalanceScore::calculate(&coupling);
1098 assert!(score.is_balanced(), "Score: {}", score.score);
1099 }
1100
1101 #[test]
1102 fn test_balance_ideal_far() {
1103 let coupling = make_coupling(
1105 IntegrationStrength::Contract,
1106 Distance::DifferentCrate,
1107 Volatility::Low,
1108 );
1109 let score = BalanceScore::calculate(&coupling);
1110 assert!(score.is_balanced(), "Score: {}", score.score);
1111 }
1112
1113 #[test]
1114 fn test_balance_bad_global_complexity() {
1115 let coupling = make_coupling(
1117 IntegrationStrength::Intrusive,
1118 Distance::DifferentCrate,
1119 Volatility::Low,
1120 );
1121 let score = BalanceScore::calculate(&coupling);
1122 assert!(
1123 score.needs_refactoring(),
1124 "Score: {}, should need refactoring",
1125 score.score
1126 );
1127 }
1128
1129 #[test]
1130 fn test_balance_bad_cascading() {
1131 let coupling = make_coupling(
1133 IntegrationStrength::Intrusive,
1134 Distance::SameModule,
1135 Volatility::High,
1136 );
1137 let score = BalanceScore::calculate(&coupling);
1138 assert!(
1139 !score.is_balanced(),
1140 "Score: {}, should not be balanced due to volatility",
1141 score.score
1142 );
1143 }
1144
1145 #[test]
1146 fn test_identify_global_complexity() {
1147 let coupling = make_coupling(
1150 IntegrationStrength::Intrusive,
1151 Distance::DifferentModule,
1152 Volatility::Low,
1153 );
1154 let issues = identify_issues(&coupling);
1155 assert!(
1156 !issues.is_empty(),
1157 "Should identify global complexity issue for internal cross-module coupling"
1158 );
1159 assert!(
1160 issues
1161 .iter()
1162 .any(|i| i.issue_type == IssueType::GlobalComplexity)
1163 );
1164 }
1165
1166 #[test]
1167 fn test_external_crates_are_skipped() {
1168 let coupling = make_coupling(
1170 IntegrationStrength::Intrusive,
1171 Distance::DifferentCrate,
1172 Volatility::Low,
1173 );
1174 let issues = identify_issues(&coupling);
1175 assert!(
1176 issues.is_empty(),
1177 "External crate dependencies should be skipped"
1178 );
1179 }
1180
1181 #[test]
1182 fn test_identify_cascading_change() {
1183 let coupling = make_coupling(
1185 IntegrationStrength::Intrusive,
1186 Distance::SameModule,
1187 Volatility::High,
1188 );
1189 let issues = identify_issues(&coupling);
1190 assert!(
1191 issues
1192 .iter()
1193 .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1194 "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1195 );
1196 }
1197
1198 #[test]
1199 fn test_identify_inappropriate_intimacy() {
1200 let coupling = make_coupling(
1203 IntegrationStrength::Intrusive,
1204 Distance::DifferentModule,
1205 Volatility::Low,
1206 );
1207 let issues = identify_issues(&coupling);
1208 assert!(
1209 issues
1210 .iter()
1211 .any(|i| i.issue_type == IssueType::GlobalComplexity),
1212 "Intrusive + DifferentModule should detect GlobalComplexity"
1213 );
1214 }
1215
1216 #[test]
1217 fn test_no_issues_for_balanced() {
1218 let coupling = make_coupling(
1220 IntegrationStrength::Model,
1221 Distance::DifferentModule,
1222 Volatility::Low,
1223 );
1224 let issues = identify_issues(&coupling);
1225 assert!(
1227 issues.is_empty(),
1228 "Model coupling should not generate issues"
1229 );
1230 }
1231
1232 #[test]
1233 fn test_health_grade_calculation() {
1234 let mut issues = HashMap::new();
1235
1236 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::A);
1238
1239 assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1241
1242 issues.insert(Severity::High, 1);
1244 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1245
1246 issues.clear();
1248 issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1250
1251 issues.clear();
1253 issues.insert(Severity::Critical, 1);
1254 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1255
1256 issues.clear();
1258 issues.insert(Severity::Critical, 4);
1259 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1260
1261 issues.clear();
1263 issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1265
1266 issues.clear();
1268 issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1270 }
1271}