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 S, A, B, C, D, F, }
910
911impl std::fmt::Display for HealthGrade {
912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913 match self {
914 HealthGrade::S => write!(f, "S (Over-optimized! Real code has some issues. Ship it!)"),
915 HealthGrade::A => write!(f, "A (Well-balanced)"),
916 HealthGrade::B => write!(f, "B (Healthy)"),
917 HealthGrade::C => write!(f, "C (Room for improvement)"),
918 HealthGrade::D => write!(f, "D (Attention needed)"),
919 HealthGrade::F => write!(f, "F (Immediate action required)"),
920 }
921 }
922}
923
924fn calculate_health_grade(
932 issues_by_severity: &HashMap<Severity, usize>,
933 internal_couplings: usize,
934) -> HealthGrade {
935 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
936 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
937 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
938
939 if internal_couplings == 0 {
941 return HealthGrade::B;
942 }
943
944 if critical > 3 {
946 return HealthGrade::F;
947 }
948
949 let high_density = high as f64 / internal_couplings as f64;
951 let medium_density = medium as f64 / internal_couplings as f64;
952 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
953
954 if critical > 0 || high_density > 0.05 {
956 return HealthGrade::D;
957 }
958
959 if high > 0 || medium_density > 0.25 {
962 return HealthGrade::C;
963 }
964
965 if medium_density > 0.10 || total_issue_density > 0.15 {
967 return HealthGrade::B;
968 }
969
970 if high == 0 && medium_density <= 0.05 && internal_couplings >= 20 {
973 return HealthGrade::S;
974 }
975
976 if high == 0 && medium_density <= 0.10 && internal_couplings >= 10 {
979 return HealthGrade::A;
980 }
981
982 HealthGrade::B
984}
985
986#[derive(Debug)]
988pub struct ProjectBalanceReport {
989 pub total_couplings: usize,
990 pub balanced_count: usize,
991 pub needs_review: usize,
992 pub needs_refactoring: usize,
993 pub average_score: f64,
994 pub health_grade: HealthGrade,
995 pub issues_by_severity: HashMap<Severity, usize>,
996 pub issues_by_type: HashMap<IssueType, usize>,
997 pub issues: Vec<CouplingIssue>,
998 pub top_priorities: Vec<CouplingIssue>,
999}
1000
1001impl ProjectBalanceReport {
1002 fn with_top_priorities(mut self, n: usize) -> Self {
1004 self.top_priorities = self.issues.iter().take(n).cloned().collect();
1005 self
1006 }
1007
1008 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
1010 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
1011 for issue in &self.issues {
1012 grouped.entry(issue.issue_type).or_default().push(issue);
1013 }
1014 grouped
1015 }
1016}
1017
1018pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
1023 let internal_scores: Vec<f64> = metrics
1025 .couplings
1026 .iter()
1027 .filter(|c| c.distance != Distance::DifferentCrate)
1028 .map(|c| BalanceScore::calculate(c).score)
1029 .collect();
1030
1031 if internal_scores.is_empty() {
1032 return 1.0; }
1034
1035 internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
1036}
1037
1038fn extract_type_name(path: &str) -> String {
1041 path.split("::")
1042 .last()
1043 .unwrap_or(path)
1044 .chars()
1045 .enumerate()
1046 .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
1047 .collect()
1048}
1049
1050pub fn strength_label(strength: IntegrationStrength) -> &'static str {
1052 match strength {
1053 IntegrationStrength::Intrusive => "Intrusive",
1054 IntegrationStrength::Functional => "Functional",
1055 IntegrationStrength::Model => "Model",
1056 IntegrationStrength::Contract => "Contract",
1057 }
1058}
1059
1060pub fn distance_label(distance: Distance) -> &'static str {
1062 match distance {
1063 Distance::SameFunction => "same function",
1064 Distance::SameModule => "same module",
1065 Distance::DifferentModule => "different module",
1066 Distance::DifferentCrate => "external crate",
1067 }
1068}
1069
1070pub fn volatility_label(volatility: Volatility) -> &'static str {
1072 match volatility {
1073 Volatility::Low => "rarely",
1074 Volatility::Medium => "sometimes",
1075 Volatility::High => "frequently",
1076 }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use super::*;
1082
1083 fn make_coupling(
1084 strength: IntegrationStrength,
1085 distance: Distance,
1086 volatility: Volatility,
1087 ) -> CouplingMetrics {
1088 CouplingMetrics::new(
1089 "source::module".to_string(),
1090 "target::module".to_string(),
1091 strength,
1092 distance,
1093 volatility,
1094 )
1095 }
1096
1097 #[test]
1098 fn test_balance_ideal_close() {
1099 let coupling = make_coupling(
1101 IntegrationStrength::Intrusive,
1102 Distance::SameModule,
1103 Volatility::Low,
1104 );
1105 let score = BalanceScore::calculate(&coupling);
1106 assert!(score.is_balanced(), "Score: {}", score.score);
1107 }
1108
1109 #[test]
1110 fn test_balance_ideal_far() {
1111 let coupling = make_coupling(
1113 IntegrationStrength::Contract,
1114 Distance::DifferentCrate,
1115 Volatility::Low,
1116 );
1117 let score = BalanceScore::calculate(&coupling);
1118 assert!(score.is_balanced(), "Score: {}", score.score);
1119 }
1120
1121 #[test]
1122 fn test_balance_bad_global_complexity() {
1123 let coupling = make_coupling(
1125 IntegrationStrength::Intrusive,
1126 Distance::DifferentCrate,
1127 Volatility::Low,
1128 );
1129 let score = BalanceScore::calculate(&coupling);
1130 assert!(
1131 score.needs_refactoring(),
1132 "Score: {}, should need refactoring",
1133 score.score
1134 );
1135 }
1136
1137 #[test]
1138 fn test_balance_bad_cascading() {
1139 let coupling = make_coupling(
1141 IntegrationStrength::Intrusive,
1142 Distance::SameModule,
1143 Volatility::High,
1144 );
1145 let score = BalanceScore::calculate(&coupling);
1146 assert!(
1147 !score.is_balanced(),
1148 "Score: {}, should not be balanced due to volatility",
1149 score.score
1150 );
1151 }
1152
1153 #[test]
1154 fn test_identify_global_complexity() {
1155 let coupling = make_coupling(
1158 IntegrationStrength::Intrusive,
1159 Distance::DifferentModule,
1160 Volatility::Low,
1161 );
1162 let issues = identify_issues(&coupling);
1163 assert!(
1164 !issues.is_empty(),
1165 "Should identify global complexity issue for internal cross-module coupling"
1166 );
1167 assert!(
1168 issues
1169 .iter()
1170 .any(|i| i.issue_type == IssueType::GlobalComplexity)
1171 );
1172 }
1173
1174 #[test]
1175 fn test_external_crates_are_skipped() {
1176 let coupling = make_coupling(
1178 IntegrationStrength::Intrusive,
1179 Distance::DifferentCrate,
1180 Volatility::Low,
1181 );
1182 let issues = identify_issues(&coupling);
1183 assert!(
1184 issues.is_empty(),
1185 "External crate dependencies should be skipped"
1186 );
1187 }
1188
1189 #[test]
1190 fn test_identify_cascading_change() {
1191 let coupling = make_coupling(
1193 IntegrationStrength::Intrusive,
1194 Distance::SameModule,
1195 Volatility::High,
1196 );
1197 let issues = identify_issues(&coupling);
1198 assert!(
1199 issues
1200 .iter()
1201 .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1202 "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_identify_inappropriate_intimacy() {
1208 let coupling = make_coupling(
1211 IntegrationStrength::Intrusive,
1212 Distance::DifferentModule,
1213 Volatility::Low,
1214 );
1215 let issues = identify_issues(&coupling);
1216 assert!(
1217 issues
1218 .iter()
1219 .any(|i| i.issue_type == IssueType::GlobalComplexity),
1220 "Intrusive + DifferentModule should detect GlobalComplexity"
1221 );
1222 }
1223
1224 #[test]
1225 fn test_no_issues_for_balanced() {
1226 let coupling = make_coupling(
1228 IntegrationStrength::Model,
1229 Distance::DifferentModule,
1230 Volatility::Low,
1231 );
1232 let issues = identify_issues(&coupling);
1233 assert!(
1235 issues.is_empty(),
1236 "Model coupling should not generate issues"
1237 );
1238 }
1239
1240 #[test]
1241 fn test_health_grade_calculation() {
1242 let mut issues = HashMap::new();
1243
1244 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::S);
1246
1247 assert_eq!(calculate_health_grade(&issues, 15), HealthGrade::A);
1249
1250 assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1252
1253 issues.insert(Severity::High, 1);
1255 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1256
1257 issues.clear();
1259 issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1261
1262 issues.clear();
1264 issues.insert(Severity::Critical, 1);
1265 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1266
1267 issues.clear();
1269 issues.insert(Severity::Critical, 4);
1270 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1271
1272 issues.clear();
1274 issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1276
1277 issues.clear();
1279 issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1281 }
1282}