1use std::collections::HashMap;
2
3use super::issue::CouplingIssue;
4use super::issue_type::IssueType;
5use super::rationale::{GradeDimension, GradeRationale, IssueTypeContribution};
6use super::severity::Severity;
7
8pub(crate) fn build_grade_rationale(
9 issues: &[CouplingIssue],
10 internal_couplings: usize,
11 japanese: bool,
12) -> GradeRationale {
13 if issues.is_empty() {
14 let summary = if japanese {
15 if internal_couplings == 0 {
16 "問題密度の採点に使える内部結合はありませんでした。".to_string()
17 } else {
18 format!(
19 "{} 件の内部結合に検出対象の問題はありません。問題密度が低いため、このグレードになっています。",
20 internal_couplings
21 )
22 }
23 } else if internal_couplings == 0 {
24 "No internal couplings were available for issue-density scoring.".to_string()
25 } else {
26 format!(
27 "No surfaced coupling issues across {} internal coupling(s); grade reflects low issue density.",
28 internal_couplings
29 )
30 };
31 return GradeRationale {
32 summary,
33 ..GradeRationale::empty()
34 };
35 }
36
37 let mut by_type: HashMap<IssueType, (usize, Severity, usize)> = HashMap::new();
38 let mut by_dimension: HashMap<GradeDimension, usize> = HashMap::new();
39 let mut high_or_critical = 0;
40
41 for issue in issues {
42 let weight = severity_weight(issue.severity);
43 let entry = by_type
44 .entry(issue.issue_type)
45 .or_insert((0, issue.severity, 0));
46 entry.0 += 1;
47 entry.1 = entry.1.max(issue.severity);
48 entry.2 += weight;
49 *by_dimension
50 .entry(dimension_for_issue(issue.issue_type))
51 .or_default() += weight;
52
53 if issue.severity >= Severity::High {
54 high_or_critical += 1;
55 }
56 }
57
58 let mut ranked_types: Vec<_> = by_type
59 .into_iter()
60 .map(|(issue_type, (count, highest_severity, weighted_score))| {
61 (
62 IssueTypeContribution {
63 issue_type,
64 count,
65 highest_severity,
66 },
67 weighted_score,
68 )
69 })
70 .collect();
71 ranked_types.sort_by(|a, b| {
72 b.1.cmp(&a.1)
73 .then_with(|| b.0.count.cmp(&a.0.count))
74 .then_with(|| a.0.issue_type.to_string().cmp(&b.0.issue_type.to_string()))
75 });
76
77 let top_issue_types: Vec<_> = ranked_types
78 .into_iter()
79 .take(3)
80 .map(|(contribution, _)| contribution)
81 .collect();
82
83 let dominant_dimension = by_dimension
84 .into_iter()
85 .max_by_key(|(_, score)| *score)
86 .map(|(dimension, _)| dimension);
87
88 let volatility_issue_count = issues
89 .iter()
90 .filter(|issue| dimension_for_issue(issue.issue_type) == GradeDimension::Volatility)
91 .count();
92 let accidental_count = issues
93 .iter()
94 .filter(|issue| issue.issue_type == IssueType::AccidentalVolatility)
95 .count();
96 let volatility_note = if volatility_issue_count > 0 {
97 let accidental_suffix = if accidental_count > 0 {
98 if japanese {
99 format!(" (偶発的な変更頻度 {} 件を含む)", accidental_count)
100 } else {
101 format!(
102 ", including {} accidental-volatility finding(s)",
103 accidental_count
104 )
105 }
106 } else {
107 String::new()
108 };
109 if japanese {
110 Some(format!(
111 "変更頻度/チャーンが {} 件の問題として影響しています{}。",
112 volatility_issue_count, accidental_suffix
113 ))
114 } else {
115 Some(format!(
116 "Volatility/churn contributes through {} issue(s){}.",
117 volatility_issue_count, accidental_suffix
118 ))
119 }
120 } else {
121 None
122 };
123
124 let top_phrase = top_issue_types
125 .iter()
126 .map(|item| {
127 if japanese {
128 format!(
129 "{} ({})",
130 issue_type_japanese_label(item.issue_type),
131 item.count
132 )
133 } else {
134 format!("{} ({})", item.issue_type, item.count)
135 }
136 })
137 .collect::<Vec<_>>()
138 .join(", ");
139 let severity_phrase = if japanese {
140 if high_or_critical > 0 {
141 format!("高/緊急の問題 {} 件", high_or_critical)
142 } else {
143 format!("中/低の問題 {} 件", issues.len())
144 }
145 } else if high_or_critical > 0 {
146 format!("{} high/critical issue(s)", high_or_critical)
147 } else {
148 format!("{} medium/low issue(s)", issues.len())
149 };
150 let dimension_phrase = dominant_dimension
151 .map(|dimension| {
152 if japanese {
153 format!(
154 "。最大の要因は{}です",
155 grade_dimension_japanese_label(dimension)
156 )
157 } else {
158 format!("; {} is the largest contributor", dimension)
159 }
160 })
161 .unwrap_or_default();
162 let note_phrase = volatility_note
163 .as_ref()
164 .map(|note| {
165 if japanese {
166 note.clone()
167 } else {
168 format!(" {}", note)
169 }
170 })
171 .unwrap_or_default();
172
173 let summary = if japanese {
174 format!(
175 "{}が主な理由です。特に {} が目立ちます{}。{note_phrase}",
176 severity_phrase, top_phrase, dimension_phrase
177 )
178 } else {
179 format!(
180 "Driven by {}, led by {}{}.{note_phrase}",
181 severity_phrase, top_phrase, dimension_phrase
182 )
183 };
184
185 GradeRationale {
186 summary,
187 top_issue_types,
188 dominant_dimension,
189 volatility_note,
190 }
191}
192
193fn grade_dimension_japanese_label(dimension: GradeDimension) -> &'static str {
194 match dimension {
195 GradeDimension::Strength => "結合強度",
196 GradeDimension::Distance => "距離",
197 GradeDimension::Volatility => "変更頻度",
198 }
199}
200
201fn issue_type_japanese_label(issue_type: IssueType) -> &'static str {
202 match issue_type {
203 IssueType::GlobalComplexity => "グローバル複雑性",
204 IssueType::CascadingChangeRisk => "変更波及リスク",
205 IssueType::InappropriateIntimacy => "不適切な親密さ",
206 IssueType::HighEfferentCoupling => "出力依存過多",
207 IssueType::HighAfferentCoupling => "入力依存過多",
208 IssueType::UnnecessaryAbstraction => "過剰な抽象化",
209 IssueType::CircularDependency => "循環依存",
210 IssueType::HiddenCoupling => "隠れた結合",
211 IssueType::AccidentalVolatility => "偶発的な変更頻度",
212 IssueType::ScatteredExternalCoupling => "外部クレート結合の分散",
213 IssueType::ShallowModule => "浅いモジュール",
214 IssueType::PassThroughMethod => "パススルーメソッド",
215 IssueType::HighCognitiveLoad => "高認知負荷",
216 IssueType::GodModule => "神モジュール",
217 IssueType::PublicFieldExposure => "公開フィールド",
218 IssueType::PrimitiveObsession => "プリミティブ過多",
219 }
220}
221
222fn severity_weight(severity: Severity) -> usize {
223 match severity {
224 Severity::Critical => 4,
225 Severity::High => 3,
226 Severity::Medium => 2,
227 Severity::Low => 1,
228 }
229}
230
231fn dimension_for_issue(issue_type: IssueType) -> GradeDimension {
232 match issue_type {
233 IssueType::CascadingChangeRisk
234 | IssueType::HiddenCoupling
235 | IssueType::AccidentalVolatility => GradeDimension::Volatility,
236 IssueType::InappropriateIntimacy
237 | IssueType::PublicFieldExposure
238 | IssueType::PrimitiveObsession
239 | IssueType::ShallowModule
240 | IssueType::PassThroughMethod => GradeDimension::Strength,
241 IssueType::GlobalComplexity
242 | IssueType::ScatteredExternalCoupling
243 | IssueType::HighEfferentCoupling
244 | IssueType::HighAfferentCoupling
245 | IssueType::UnnecessaryAbstraction
246 | IssueType::CircularDependency
247 | IssueType::HighCognitiveLoad
248 | IssueType::GodModule => GradeDimension::Distance,
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum HealthGrade {
255 S,
257 A,
259 B,
261 C,
263 D,
265 F,
267}
268
269impl HealthGrade {
270 pub fn letter(&self) -> char {
275 match self {
276 HealthGrade::S => 'S',
277 HealthGrade::A => 'A',
278 HealthGrade::B => 'B',
279 HealthGrade::C => 'C',
280 HealthGrade::D => 'D',
281 HealthGrade::F => 'F',
282 }
283 }
284}
285
286impl std::fmt::Display for HealthGrade {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 HealthGrade::S => write!(f, "S (Over-optimized! Real code has some issues. Ship it!)"),
290 HealthGrade::A => write!(f, "A (Well-balanced)"),
291 HealthGrade::B => write!(f, "B (Healthy)"),
292 HealthGrade::C => write!(f, "C (Room for improvement)"),
293 HealthGrade::D => write!(f, "D (Attention needed)"),
294 HealthGrade::F => write!(f, "F (Immediate action required)"),
295 }
296 }
297}
298
299pub(crate) fn calculate_health_grade(
307 issues_by_severity: &HashMap<Severity, usize>,
308 internal_couplings: usize,
309) -> HealthGrade {
310 let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
311 let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
312 let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
313
314 if internal_couplings == 0 {
316 return HealthGrade::B;
317 }
318
319 if critical > 3 {
321 return HealthGrade::F;
322 }
323
324 let high_density = high as f64 / internal_couplings as f64;
326 let medium_density = medium as f64 / internal_couplings as f64;
327 let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
328
329 if critical > 0 || high_density > 0.05 {
331 return HealthGrade::D;
332 }
333
334 if high > 0 || medium_density > 0.25 {
337 return HealthGrade::C;
338 }
339
340 if medium_density > 0.10 || total_issue_density > 0.15 {
342 return HealthGrade::B;
343 }
344
345 if high == 0 && medium_density <= 0.05 && internal_couplings >= 20 {
348 return HealthGrade::S;
349 }
350
351 if high == 0 && medium_density <= 0.10 && internal_couplings >= 10 {
354 return HealthGrade::A;
355 }
356
357 HealthGrade::B
359}
360
361#[derive(Debug)]
363pub struct ProjectBalanceReport {
364 pub total_couplings: usize,
366 pub balanced_count: usize,
368 pub needs_review: usize,
370 pub needs_refactoring: usize,
372 pub average_score: f64,
374 pub health_grade: HealthGrade,
376 pub issues_by_severity: HashMap<Severity, usize>,
378 pub issues_by_type: HashMap<IssueType, usize>,
380 pub issues: Vec<CouplingIssue>,
382 pub top_priorities: Vec<CouplingIssue>,
384 pub grade_rationale: GradeRationale,
386}
387
388impl ProjectBalanceReport {
389 pub(crate) fn with_top_priorities(mut self, n: usize) -> Self {
391 self.top_priorities = self.issues.iter().take(n).cloned().collect();
392 self
393 }
394
395 pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
397 let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
398 for issue in &self.issues {
399 grouped.entry(issue.issue_type).or_default().push(issue);
400 }
401 grouped
402 }
403}