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 pub exclude_tests: bool,
392 pub prelude_module_count: usize,
394}
395
396impl Default for IssueThresholds {
397 fn default() -> Self {
398 Self {
399 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, exclude_tests: false, prelude_module_count: 0, }
413 }
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub enum CrateStability {
419 Fundamental,
421 Stable,
423 Infrastructure,
425 Normal,
427}
428
429pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
431 let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
433
434 match base_name {
435 "std" | "core" | "alloc" => CrateStability::Fundamental,
437
438 "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,
450
451 "tokio" | "async-std" | "smol" | "async-trait" | "futures" | "futures-util" | "tracing" | "tracing-subscriber" | "tracing-opentelemetry" | "opentelemetry" | "opentelemetry-otlp" | "opentelemetry_sdk" |
458 "hyper" | "reqwest" | "http" | "tonic" | "prost" | "sqlx" | "diesel" | "sea-orm" | "clap" | "structopt" => CrateStability::Infrastructure,
463
464 _ => CrateStability::Normal,
466 }
467}
468
469pub fn should_skip_crate(crate_name: &str) -> bool {
471 matches!(
472 classify_crate_stability(crate_name),
473 CrateStability::Fundamental
474 )
475}
476
477pub fn should_reduce_severity(crate_name: &str) -> bool {
479 matches!(
480 classify_crate_stability(crate_name),
481 CrateStability::Stable | CrateStability::Infrastructure
482 )
483}
484
485pub fn is_external_crate(target: &str, source: &str) -> bool {
488 let target_prefix = target.split("::").next().unwrap_or(target);
493 let source_prefix = source.split("::").next().unwrap_or(source);
494
495 if target_prefix == source_prefix {
497 return false;
498 }
499
500 let stability = classify_crate_stability(target);
503 matches!(
504 stability,
505 CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
506 )
507}
508
509pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
511 identify_issues_with_thresholds(coupling, &IssueThresholds::default())
512}
513
514pub fn identify_issues_with_thresholds(
516 coupling: &CouplingMetrics,
517 _thresholds: &IssueThresholds,
518) -> Vec<CouplingIssue> {
519 let mut issues = Vec::new();
520
521 if coupling.distance == Distance::DifferentCrate {
524 return issues;
525 }
526
527 let balance = BalanceScore::calculate(coupling);
528
529 if coupling.strength == IntegrationStrength::Intrusive
532 && coupling.distance == Distance::DifferentModule
533 {
534 issues.push(CouplingIssue {
535 issue_type: IssueType::GlobalComplexity,
536 severity: Severity::Medium, source: coupling.source.clone(),
538 target: coupling.target.clone(),
539 description: format!(
540 "Intrusive coupling to {} across module boundary",
541 coupling.target,
542 ),
543 refactoring: RefactoringAction::IntroduceTrait {
544 suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
545 methods: vec!["// Extract required methods".to_string()],
546 },
547 balance_score: balance.score,
548 });
549 }
550
551 if coupling.strength == IntegrationStrength::Intrusive
554 && coupling.volatility == Volatility::High
555 {
556 issues.push(CouplingIssue {
557 issue_type: IssueType::CascadingChangeRisk,
558 severity: Severity::High,
559 source: coupling.source.clone(),
560 target: coupling.target.clone(),
561 description: format!(
562 "Intrusive coupling to frequently-changed component {}",
563 coupling.target,
564 ),
565 refactoring: RefactoringAction::StabilizeInterface {
566 interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
567 },
568 balance_score: balance.score,
569 });
570 }
571
572 if coupling.strength == IntegrationStrength::Intrusive
575 && coupling.distance == Distance::DifferentModule
576 && balance.score < 0.5
577 {
578 if !issues
580 .iter()
581 .any(|i| i.issue_type == IssueType::GlobalComplexity)
582 {
583 issues.push(CouplingIssue {
584 issue_type: IssueType::InappropriateIntimacy,
585 severity: Severity::Medium,
586 source: coupling.source.clone(),
587 target: coupling.target.clone(),
588 description: format!(
589 "Direct internal access to {} across module boundary",
590 coupling.target,
591 ),
592 refactoring: RefactoringAction::IntroduceTrait {
593 suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
594 methods: vec!["// Expose only necessary operations".to_string()],
595 },
596 balance_score: balance.score,
597 });
598 }
599 }
600
601 issues
606}
607
608pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
610 analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
611}
612
613pub fn analyze_project_balance_with_thresholds(
615 metrics: &ProjectMetrics,
616 thresholds: &IssueThresholds,
617) -> ProjectBalanceReport {
618 let thresholds = thresholds.clone();
619 let mut all_issues = Vec::new();
620 let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
621 let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
622
623 for coupling in &metrics.couplings {
626 let score = BalanceScore::calculate(coupling);
627 all_balance_scores.push(score.clone());
628
629 if coupling.distance != Distance::DifferentCrate {
631 internal_balance_scores.push(score);
632 let issues = identify_issues_with_thresholds(coupling, &thresholds);
633 all_issues.extend(issues);
634 }
635 }
636
637 let module_issues = analyze_module_coupling(metrics, &thresholds);
639 all_issues.extend(module_issues);
640
641 let rust_issues = analyze_rust_patterns(metrics, &thresholds);
643 all_issues.extend(rust_issues);
644
645 if thresholds.strict_mode {
647 all_issues.retain(|issue| issue.severity >= Severity::Medium);
648 }
649
650 all_issues.sort_by(|a, b| {
652 b.severity
653 .cmp(&a.severity)
654 .then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
655 });
656
657 let total_couplings = metrics.couplings.len();
659 let internal_couplings = internal_balance_scores.len();
660
661 let balanced_count = internal_balance_scores
662 .iter()
663 .filter(|s| s.is_balanced())
664 .count();
665 let needs_review = internal_balance_scores
666 .iter()
667 .filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
668 .count();
669 let needs_refactoring = internal_balance_scores
670 .iter()
671 .filter(|s| s.needs_refactoring())
672 .count();
673
674 let average_score = if internal_balance_scores.is_empty() {
676 1.0 } else {
678 internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
679 / internal_balance_scores.len() as f64
680 };
681
682 let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
684 for issue in &all_issues {
685 *issues_by_severity.entry(issue.severity).or_insert(0) += 1;
686 }
687
688 let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
690 for issue in &all_issues {
691 *issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
692 }
693
694 let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
696
697 ProjectBalanceReport {
698 total_couplings,
699 balanced_count,
700 needs_review,
701 needs_refactoring,
702 average_score,
703 health_grade,
704 issues_by_severity,
705 issues_by_type,
706 issues: all_issues,
707 top_priorities: Vec::new(), }
709 .with_top_priorities(5) }
711
712fn analyze_module_coupling(
714 metrics: &ProjectMetrics,
715 thresholds: &IssueThresholds,
716) -> Vec<CouplingIssue> {
717 let mut issues = Vec::new();
718
719 let mut efferent: HashMap<&str, usize> = HashMap::new();
722 let mut afferent: HashMap<&str, usize> = HashMap::new();
723
724 for coupling in &metrics.couplings {
725 if coupling.distance == Distance::DifferentCrate {
727 continue;
728 }
729
730 *efferent.entry(&coupling.source).or_insert(0) += 1;
731 *afferent.entry(&coupling.target).or_insert(0) += 1;
732 }
733
734 for (module, count) in &efferent {
736 if *count > thresholds.max_dependencies {
737 issues.push(CouplingIssue {
738 issue_type: IssueType::HighEfferentCoupling,
739 severity: if *count > thresholds.max_dependencies * 2 {
740 Severity::High
741 } else {
742 Severity::Medium
743 },
744 source: module.to_string(),
745 target: format!("{} dependencies", count),
746 description: format!(
747 "Module {} depends on {} other components (threshold: {})",
748 module, count, thresholds.max_dependencies
749 ),
750 refactoring: RefactoringAction::SplitModule {
751 suggested_modules: vec![
752 format!("{}_core", module),
753 format!("{}_integration", module),
754 ],
755 },
756 balance_score: 1.0
757 - (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
758 });
759 }
760 }
761
762 for (module, count) in &afferent {
765 if *count > thresholds.max_dependents {
766 issues.push(CouplingIssue {
767 issue_type: IssueType::HighAfferentCoupling,
768 severity: if *count > thresholds.max_dependents * 2 {
769 Severity::High
770 } else {
771 Severity::Medium
772 },
773 source: format!("{} dependents", count),
774 target: module.to_string(),
775 description: format!(
776 "Module {} is depended on by {} other components (threshold: {})",
777 module, count, thresholds.max_dependents
778 ),
779 refactoring: RefactoringAction::IntroduceTrait {
780 suggested_name: format!("{}Interface", extract_type_name(module)),
781 methods: vec!["// Define stable public API".to_string()],
782 },
783 balance_score: 1.0
784 - (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
785 });
786 }
787 }
788
789 issues
790}
791
792fn analyze_rust_patterns(
794 metrics: &ProjectMetrics,
795 thresholds: &IssueThresholds,
796) -> Vec<CouplingIssue> {
797 let mut issues = Vec::new();
798
799 for (module_name, module) in &metrics.modules {
801 let func_count = if thresholds.exclude_tests {
803 module
804 .function_count()
805 .saturating_sub(module.test_function_count)
806 } else {
807 module.function_count()
808 };
809 let type_count = module.type_definitions.len();
810 let impl_count = module.trait_impl_count + module.inherent_impl_count;
811
812 let is_god_module = func_count > thresholds.max_functions
814 || type_count > thresholds.max_types
815 || impl_count > thresholds.max_impls;
816
817 if is_god_module {
818 issues.push(CouplingIssue {
819 issue_type: IssueType::GodModule,
820 severity: if func_count > thresholds.max_functions * 2
821 || type_count > thresholds.max_types * 2
822 {
823 Severity::High
824 } else {
825 Severity::Medium
826 },
827 source: module_name.clone(),
828 target: format!(
829 "{} functions, {} types, {} impls",
830 func_count, type_count, impl_count
831 ),
832 description: format!(
833 "Module {} has too many responsibilities (functions: {}/{}, types: {}/{}, impls: {}/{})",
834 module_name,
835 func_count, thresholds.max_functions,
836 type_count, thresholds.max_types,
837 impl_count, thresholds.max_impls,
838 ),
839 refactoring: RefactoringAction::SplitModule {
840 suggested_modules: vec![
841 format!("{}_core", module_name),
842 format!("{}_helpers", module_name),
843 ],
844 },
845 balance_score: 0.5,
846 });
847 }
848
849 for type_def in module.type_definitions.values() {
851 if type_def.public_field_count > 0
852 && !type_def.is_trait
853 && type_def.visibility == crate::metrics::Visibility::Public
854 {
855 issues.push(CouplingIssue {
856 issue_type: IssueType::PublicFieldExposure,
857 severity: Severity::Low,
858 source: format!("{}::{}", module_name, type_def.name),
859 target: format!("{} public fields", type_def.public_field_count),
860 description: format!(
861 "Type {} has {} public field(s). Consider using getter methods.",
862 type_def.name, type_def.public_field_count
863 ),
864 refactoring: RefactoringAction::AddGetters {
865 fields: vec!["// Add getter methods".to_string()],
866 },
867 balance_score: 0.7,
868 });
869 }
870 }
871
872 for func_def in module.function_definitions.values() {
874 if func_def.primitive_param_count >= thresholds.min_primitive_params
875 && func_def.param_count >= thresholds.min_primitive_params
876 {
877 let ratio = func_def.primitive_param_count as f64 / func_def.param_count as f64;
878 if ratio >= 0.6 {
879 issues.push(CouplingIssue {
880 issue_type: IssueType::PrimitiveObsession,
881 severity: Severity::Low,
882 source: format!("{}::{}", module_name, func_def.name),
883 target: format!(
884 "{}/{} primitive params",
885 func_def.primitive_param_count, func_def.param_count
886 ),
887 description: format!(
888 "Function {} has {} primitive parameters. Consider newtype pattern.",
889 func_def.name, func_def.primitive_param_count
890 ),
891 refactoring: RefactoringAction::IntroduceNewtype {
892 suggested_name: format!("{}Params", capitalize_first(&func_def.name)),
893 wrapped_type: "// Group related parameters".to_string(),
894 },
895 balance_score: 0.7,
896 });
897 }
898 }
899 }
900 }
901
902 issues
903}
904
905fn capitalize_first(s: &str) -> String {
907 let mut chars = s.chars();
908 match chars.next() {
909 None => String::new(),
910 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
911 }
912}
913
914#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub enum HealthGrade {
917 S, A, B, C, D, F, }
924
925impl std::fmt::Display for HealthGrade {
926 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
927 match self {
928 HealthGrade::S => write!(f, "S (Over-optimized! Real code has some issues. Ship it!)"),
929 HealthGrade::A => write!(f, "A (Well-balanced)"),
930 HealthGrade::B => write!(f, "B (Healthy)"),
931 HealthGrade::C => write!(f, "C (Room for improvement)"),
932 HealthGrade::D => write!(f, "D (Attention needed)"),
933 HealthGrade::F => write!(f, "F (Immediate action required)"),
934 }
935 }
936}
937
938fn calculate_health_grade(
946 issues_by_severity: &HashMap<Severity, usize>,
947 internal_couplings: usize,
948) -> HealthGrade {
949 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
950 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
951 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
952
953 if internal_couplings == 0 {
955 return HealthGrade::B;
956 }
957
958 if critical > 3 {
960 return HealthGrade::F;
961 }
962
963 let high_density = high as f64 / internal_couplings as f64;
965 let medium_density = medium as f64 / internal_couplings as f64;
966 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
967
968 if critical > 0 || high_density > 0.05 {
970 return HealthGrade::D;
971 }
972
973 if high > 0 || medium_density > 0.25 {
976 return HealthGrade::C;
977 }
978
979 if medium_density > 0.10 || total_issue_density > 0.15 {
981 return HealthGrade::B;
982 }
983
984 if high == 0 && medium_density <= 0.05 && internal_couplings >= 20 {
987 return HealthGrade::S;
988 }
989
990 if high == 0 && medium_density <= 0.10 && internal_couplings >= 10 {
993 return HealthGrade::A;
994 }
995
996 HealthGrade::B
998}
999
1000#[derive(Debug)]
1002pub struct ProjectBalanceReport {
1003 pub total_couplings: usize,
1004 pub balanced_count: usize,
1005 pub needs_review: usize,
1006 pub needs_refactoring: usize,
1007 pub average_score: f64,
1008 pub health_grade: HealthGrade,
1009 pub issues_by_severity: HashMap<Severity, usize>,
1010 pub issues_by_type: HashMap<IssueType, usize>,
1011 pub issues: Vec<CouplingIssue>,
1012 pub top_priorities: Vec<CouplingIssue>,
1013}
1014
1015impl ProjectBalanceReport {
1016 fn with_top_priorities(mut self, n: usize) -> Self {
1018 self.top_priorities = self.issues.iter().take(n).cloned().collect();
1019 self
1020 }
1021
1022 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
1024 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
1025 for issue in &self.issues {
1026 grouped.entry(issue.issue_type).or_default().push(issue);
1027 }
1028 grouped
1029 }
1030}
1031
1032pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
1037 let internal_scores: Vec<f64> = metrics
1039 .couplings
1040 .iter()
1041 .filter(|c| c.distance != Distance::DifferentCrate)
1042 .map(|c| BalanceScore::calculate(c).score)
1043 .collect();
1044
1045 if internal_scores.is_empty() {
1046 return 1.0; }
1048
1049 internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
1050}
1051
1052fn extract_type_name(path: &str) -> String {
1055 path.split("::")
1056 .last()
1057 .unwrap_or(path)
1058 .chars()
1059 .enumerate()
1060 .map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
1061 .collect()
1062}
1063
1064pub fn strength_label(strength: IntegrationStrength) -> &'static str {
1066 match strength {
1067 IntegrationStrength::Intrusive => "Intrusive",
1068 IntegrationStrength::Functional => "Functional",
1069 IntegrationStrength::Model => "Model",
1070 IntegrationStrength::Contract => "Contract",
1071 }
1072}
1073
1074pub fn distance_label(distance: Distance) -> &'static str {
1076 match distance {
1077 Distance::SameFunction => "same function",
1078 Distance::SameModule => "same module",
1079 Distance::DifferentModule => "different module",
1080 Distance::DifferentCrate => "external crate",
1081 }
1082}
1083
1084pub fn volatility_label(volatility: Volatility) -> &'static str {
1086 match volatility {
1087 Volatility::Low => "rarely",
1088 Volatility::Medium => "sometimes",
1089 Volatility::High => "frequently",
1090 }
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096
1097 fn make_coupling(
1098 strength: IntegrationStrength,
1099 distance: Distance,
1100 volatility: Volatility,
1101 ) -> CouplingMetrics {
1102 CouplingMetrics::new(
1103 "source::module".to_string(),
1104 "target::module".to_string(),
1105 strength,
1106 distance,
1107 volatility,
1108 )
1109 }
1110
1111 #[test]
1112 fn test_balance_ideal_close() {
1113 let coupling = make_coupling(
1115 IntegrationStrength::Intrusive,
1116 Distance::SameModule,
1117 Volatility::Low,
1118 );
1119 let score = BalanceScore::calculate(&coupling);
1120 assert!(score.is_balanced(), "Score: {}", score.score);
1121 }
1122
1123 #[test]
1124 fn test_balance_ideal_far() {
1125 let coupling = make_coupling(
1127 IntegrationStrength::Contract,
1128 Distance::DifferentCrate,
1129 Volatility::Low,
1130 );
1131 let score = BalanceScore::calculate(&coupling);
1132 assert!(score.is_balanced(), "Score: {}", score.score);
1133 }
1134
1135 #[test]
1136 fn test_balance_bad_global_complexity() {
1137 let coupling = make_coupling(
1139 IntegrationStrength::Intrusive,
1140 Distance::DifferentCrate,
1141 Volatility::Low,
1142 );
1143 let score = BalanceScore::calculate(&coupling);
1144 assert!(
1145 score.needs_refactoring(),
1146 "Score: {}, should need refactoring",
1147 score.score
1148 );
1149 }
1150
1151 #[test]
1152 fn test_balance_bad_cascading() {
1153 let coupling = make_coupling(
1155 IntegrationStrength::Intrusive,
1156 Distance::SameModule,
1157 Volatility::High,
1158 );
1159 let score = BalanceScore::calculate(&coupling);
1160 assert!(
1161 !score.is_balanced(),
1162 "Score: {}, should not be balanced due to volatility",
1163 score.score
1164 );
1165 }
1166
1167 #[test]
1168 fn test_identify_global_complexity() {
1169 let coupling = make_coupling(
1172 IntegrationStrength::Intrusive,
1173 Distance::DifferentModule,
1174 Volatility::Low,
1175 );
1176 let issues = identify_issues(&coupling);
1177 assert!(
1178 !issues.is_empty(),
1179 "Should identify global complexity issue for internal cross-module coupling"
1180 );
1181 assert!(
1182 issues
1183 .iter()
1184 .any(|i| i.issue_type == IssueType::GlobalComplexity)
1185 );
1186 }
1187
1188 #[test]
1189 fn test_external_crates_are_skipped() {
1190 let coupling = make_coupling(
1192 IntegrationStrength::Intrusive,
1193 Distance::DifferentCrate,
1194 Volatility::Low,
1195 );
1196 let issues = identify_issues(&coupling);
1197 assert!(
1198 issues.is_empty(),
1199 "External crate dependencies should be skipped"
1200 );
1201 }
1202
1203 #[test]
1204 fn test_identify_cascading_change() {
1205 let coupling = make_coupling(
1207 IntegrationStrength::Intrusive,
1208 Distance::SameModule,
1209 Volatility::High,
1210 );
1211 let issues = identify_issues(&coupling);
1212 assert!(
1213 issues
1214 .iter()
1215 .any(|i| i.issue_type == IssueType::CascadingChangeRisk),
1216 "Intrusive coupling + High volatility should detect CascadingChangeRisk"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_identify_inappropriate_intimacy() {
1222 let coupling = make_coupling(
1225 IntegrationStrength::Intrusive,
1226 Distance::DifferentModule,
1227 Volatility::Low,
1228 );
1229 let issues = identify_issues(&coupling);
1230 assert!(
1231 issues
1232 .iter()
1233 .any(|i| i.issue_type == IssueType::GlobalComplexity),
1234 "Intrusive + DifferentModule should detect GlobalComplexity"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_no_issues_for_balanced() {
1240 let coupling = make_coupling(
1242 IntegrationStrength::Model,
1243 Distance::DifferentModule,
1244 Volatility::Low,
1245 );
1246 let issues = identify_issues(&coupling);
1247 assert!(
1249 issues.is_empty(),
1250 "Model coupling should not generate issues"
1251 );
1252 }
1253
1254 #[test]
1255 fn test_health_grade_calculation() {
1256 let mut issues = HashMap::new();
1257
1258 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::S);
1260
1261 assert_eq!(calculate_health_grade(&issues, 15), HealthGrade::A);
1263
1264 assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
1266
1267 issues.insert(Severity::High, 1);
1269 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1270
1271 issues.clear();
1273 issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1275
1276 issues.clear();
1278 issues.insert(Severity::Critical, 1);
1279 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
1280
1281 issues.clear();
1283 issues.insert(Severity::Critical, 4);
1284 assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
1285
1286 issues.clear();
1288 issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
1290
1291 issues.clear();
1293 issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
1295 }
1296}