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