1use std::io::{self, Write};
6
7use crate::balance::{
8 BalanceScore, IssueThresholds, ProjectBalanceReport, Severity,
9 analyze_project_balance_with_thresholds,
10};
11use crate::metrics::{Distance, IntegrationStrength, ProjectMetrics};
12
13pub fn generate_summary<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
15 generate_summary_with_thresholds(metrics, &IssueThresholds::default(), writer)
16}
17
18pub fn generate_summary_with_thresholds<W: Write>(
20 metrics: &ProjectMetrics,
21 thresholds: &IssueThresholds,
22 writer: &mut W,
23) -> io::Result<()> {
24 let report = analyze_project_balance_with_thresholds(metrics, thresholds);
25 let dimension_stats = metrics.calculate_dimension_stats();
26 let jp = thresholds.japanese;
27
28 let project_name = metrics.workspace_name.as_deref().unwrap_or("project");
29
30 if jp {
31 writeln!(writer, "カップリング分析: {}", project_name)?;
32 writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
33 writeln!(writer)?;
34 writeln!(
35 writer,
36 "評価: {} | スコア: {:.2}/1.00 | モジュール数: {}",
37 report.health_grade,
38 report.average_score,
39 metrics.module_count()
40 )?;
41 } else {
42 writeln!(writer, "Balanced Coupling Analysis: {}", project_name)?;
43 writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
44 writeln!(writer)?;
45 writeln!(
46 writer,
47 "Grade: {} | Score: {:.2}/1.00 | Modules: {}",
48 report.health_grade,
49 report.average_score,
50 metrics.module_count()
51 )?;
52 }
53 writeln!(writer)?;
54
55 if !metrics.couplings.is_empty() {
57 let (intr_pct, func_pct, model_pct, contract_pct) = dimension_stats.strength_percentages();
59 let (same_pct, diff_pct, ext_pct) = dimension_stats.distance_percentages();
61 let (low_pct, med_pct, high_pct) = dimension_stats.volatility_percentages();
63
64 if jp {
65 writeln!(writer, "3次元分析:")?;
66 writeln!(
67 writer,
68 " 結合強度: Contract {:.0}% / Model {:.0}% / Functional {:.0}% / Intrusive {:.0}%",
69 contract_pct, model_pct, func_pct, intr_pct
70 )?;
71 writeln!(
72 writer,
73 " (トレイト) (型) (関数) (内部アクセス)"
74 )?;
75 writeln!(
76 writer,
77 " 距離: 同一モジュール {:.0}% / 別モジュール {:.0}% / 外部 {:.0}%",
78 same_pct, diff_pct, ext_pct
79 )?;
80 writeln!(
81 writer,
82 " 変更頻度: 低 {:.0}% / 中 {:.0}% / 高 {:.0}%",
83 low_pct, med_pct, high_pct
84 )?;
85 } else {
86 writeln!(writer, "3-Dimensional Analysis:")?;
87 writeln!(
88 writer,
89 " Strength: Contract {:.0}% / Model {:.0}% / Functional {:.0}% / Intrusive {:.0}%",
90 contract_pct, model_pct, func_pct, intr_pct
91 )?;
92 writeln!(
93 writer,
94 " Distance: Same {:.0}% / Different {:.0}% / External {:.0}%",
95 same_pct, diff_pct, ext_pct
96 )?;
97 writeln!(
98 writer,
99 " Volatility: Low {:.0}% / Medium {:.0}% / High {:.0}%",
100 low_pct, med_pct, high_pct
101 )?;
102 }
103 writeln!(writer)?;
104
105 if jp {
107 writeln!(writer, "バランス状態:")?;
108 } else {
109 writeln!(writer, "Balance State:")?;
110 }
111 let bc = &dimension_stats.balance_counts;
112 let total = dimension_stats.total();
113 if bc.high_cohesion > 0 {
114 if jp {
115 writeln!(
116 writer,
117 " ✅ 高凝集 (強い結合 + 近い距離): {} ({:.0}%) ← 理想的",
118 bc.high_cohesion,
119 bc.high_cohesion as f64 / total as f64 * 100.0
120 )?;
121 } else {
122 writeln!(
123 writer,
124 " ✅ High Cohesion (strong+close): {} ({:.0}%)",
125 bc.high_cohesion,
126 bc.high_cohesion as f64 / total as f64 * 100.0
127 )?;
128 }
129 }
130 if bc.loose_coupling > 0 {
131 if jp {
132 writeln!(
133 writer,
134 " ✅ 疎結合 (弱い結合 + 遠い距離): {} ({:.0}%) ← 理想的",
135 bc.loose_coupling,
136 bc.loose_coupling as f64 / total as f64 * 100.0
137 )?;
138 } else {
139 writeln!(
140 writer,
141 " ✅ Loose Coupling (weak+far): {} ({:.0}%)",
142 bc.loose_coupling,
143 bc.loose_coupling as f64 / total as f64 * 100.0
144 )?;
145 }
146 }
147 if bc.acceptable > 0 {
148 if jp {
149 writeln!(
150 writer,
151 " 🤔 許容可能 (強い結合 + 遠い距離 + 安定): {} ({:.0}%)",
152 bc.acceptable,
153 bc.acceptable as f64 / total as f64 * 100.0
154 )?;
155 } else {
156 writeln!(
157 writer,
158 " 🤔 Acceptable (strong+far+stable): {} ({:.0}%)",
159 bc.acceptable,
160 bc.acceptable as f64 / total as f64 * 100.0
161 )?;
162 }
163 }
164 if bc.pain > 0 {
165 if jp {
166 writeln!(
167 writer,
168 " ❌ 要リファクタリング (強い結合 + 遠い距離 + 頻繁に変更): {} ({:.0}%)",
169 bc.pain,
170 bc.pain as f64 / total as f64 * 100.0
171 )?;
172 } else {
173 writeln!(
174 writer,
175 " ❌ Needs Refactoring (strong+far+volatile): {} ({:.0}%)",
176 bc.pain,
177 bc.pain as f64 / total as f64 * 100.0
178 )?;
179 }
180 }
181 if bc.local_complexity > 0 {
182 if jp {
183 writeln!(
184 writer,
185 " 🔍 局所的複雑性 (弱い結合 + 近い距離): {} ({:.0}%)",
186 bc.local_complexity,
187 bc.local_complexity as f64 / total as f64 * 100.0
188 )?;
189 } else {
190 writeln!(
191 writer,
192 " 🔍 Local Complexity (weak+close): {} ({:.0}%)",
193 bc.local_complexity,
194 bc.local_complexity as f64 / total as f64 * 100.0
195 )?;
196 }
197 }
198 writeln!(writer)?;
199 }
200
201 let critical = *report
203 .issues_by_severity
204 .get(&Severity::Critical)
205 .unwrap_or(&0);
206 let high = *report.issues_by_severity.get(&Severity::High).unwrap_or(&0);
207 let medium = *report
208 .issues_by_severity
209 .get(&Severity::Medium)
210 .unwrap_or(&0);
211 let low = *report.issues_by_severity.get(&Severity::Low).unwrap_or(&0);
212
213 if critical > 0 || high > 0 || medium > 0 || low > 0 {
214 if jp {
215 writeln!(writer, "検出された問題:")?;
216 if critical > 0 {
217 writeln!(writer, " 🔴 緊急: {} 件 (すぐに修正が必要)", critical)?;
218 }
219 if high > 0 {
220 writeln!(writer, " 🟠 高: {} 件 (早めに対処)", high)?;
221 }
222 if medium > 0 {
223 writeln!(writer, " 🟡 中: {} 件", medium)?;
224 }
225 if low > 0 {
226 writeln!(writer, " ⚪ 低: {} 件", low)?;
227 }
228 } else {
229 writeln!(writer, "Detected Issues:")?;
230 if critical > 0 {
231 writeln!(writer, " 🔴 Critical: {} (must fix)", critical)?;
232 }
233 if high > 0 {
234 writeln!(writer, " 🟠 High: {} (should fix)", high)?;
235 }
236 if medium > 0 {
237 writeln!(writer, " 🟡 Medium: {}", medium)?;
238 }
239 if low > 0 {
240 writeln!(writer, " ⚪ Low: {}", low)?;
241 }
242 }
243 writeln!(writer)?;
244 } else if thresholds.strict_mode {
245 if jp {
246 writeln!(writer, "検出された問題: なし (--all で低優先度も表示)\n")?;
247 } else {
248 writeln!(
249 writer,
250 "Detected Issues: None (use --all to see Low severity)\n"
251 )?;
252 }
253 }
254
255 if !report.top_priorities.is_empty() {
257 if jp {
258 writeln!(writer, "優先的に対処すべき問題:")?;
259 for issue in report.top_priorities.iter().take(3) {
260 let issue_jp = issue_type_japanese(issue.issue_type);
261 writeln!(writer, " - {} | {}", issue_jp, issue.source)?;
262 writeln!(
263 writer,
264 " → {}",
265 refactoring_action_japanese(&issue.refactoring)
266 )?;
267 }
268 } else {
269 writeln!(writer, "Top Priorities:")?;
270 for issue in report.top_priorities.iter().take(3) {
271 writeln!(
272 writer,
273 " - [{}] {} → {}",
274 issue.severity, issue.source, issue.target
275 )?;
276 }
277 }
278 writeln!(writer)?;
279 }
280
281 let newtype_count = metrics.total_newtype_count();
283 let type_count = metrics.total_type_count();
284 if type_count > 0 {
285 let newtype_ratio = metrics.newtype_ratio() * 100.0;
286 if jp {
287 let quality = if newtype_ratio >= 20.0 {
288 "✅ 良好"
289 } else if newtype_ratio >= 10.0 {
290 "🤔 増やすことを検討"
291 } else {
292 "⚠️ 少ない"
293 };
294 writeln!(
295 writer,
296 "Rustパターン: newtype使用率 {}/{} ({:.0}%) - {}",
297 newtype_count, type_count, newtype_ratio, quality
298 )?;
299 } else {
300 let quality = if newtype_ratio >= 20.0 {
301 "✅ Good"
302 } else if newtype_ratio >= 10.0 {
303 "🤔 Consider more"
304 } else {
305 "⚠️ Low usage"
306 };
307 writeln!(
308 writer,
309 "Rust Patterns: Newtype usage: {}/{} ({:.0}%) - {}",
310 newtype_count, type_count, newtype_ratio, quality
311 )?;
312 }
313 writeln!(writer)?;
314 }
315
316 let circular = metrics.circular_dependency_summary();
318 if circular.total_cycles > 0 {
319 if jp {
320 writeln!(
321 writer,
322 "⚠️ 循環依存: {} サイクル ({} モジュール)",
323 circular.total_cycles, circular.affected_modules
324 )?;
325 } else {
326 writeln!(
327 writer,
328 "⚠️ Circular Dependencies: {} cycles ({} modules)",
329 circular.total_cycles, circular.affected_modules
330 )?;
331 }
332 }
333
334 if jp {
336 writeln!(writer)?;
337 writeln!(writer, "設計判断ガイド (Khononov):")?;
338 writeln!(writer, " ✅ 強い結合 + 近い距離 → 高凝集 (理想的)")?;
339 writeln!(writer, " ✅ 弱い結合 + 遠い距離 → 疎結合 (理想的)")?;
340 writeln!(writer, " 🤔 強い結合 + 遠い距離 + 安定 → 許容可能")?;
341 writeln!(
342 writer,
343 " ❌ 強い結合 + 遠い距離 + 頻繁に変更 → 要リファクタリング"
344 )?;
345 }
346
347 Ok(())
348}
349
350fn issue_type_japanese(issue_type: crate::balance::IssueType) -> &'static str {
352 use crate::balance::IssueType;
353 match issue_type {
354 IssueType::GlobalComplexity => "グローバル複雑性 (遠距離への強い依存)",
355 IssueType::CascadingChangeRisk => "変更波及リスク (頻繁に変わるものへの依存)",
356 IssueType::InappropriateIntimacy => "不適切な親密さ (内部実装への依存)",
357 IssueType::HighEfferentCoupling => "出力依存過多 (多くのモジュールに依存)",
358 IssueType::HighAfferentCoupling => "入力依存過多 (多くのモジュールから依存される)",
359 IssueType::UnnecessaryAbstraction => "過剰な抽象化",
360 IssueType::CircularDependency => "循環依存",
361 IssueType::ShallowModule => "浅いモジュール",
362 IssueType::PassThroughMethod => "パススルーメソッド",
363 IssueType::HighCognitiveLoad => "高認知負荷",
364 IssueType::GodModule => "神モジュール (責務が多すぎる)",
365 IssueType::PublicFieldExposure => "公開フィールド (getterを検討)",
366 IssueType::PrimitiveObsession => "プリミティブ過多 (newtypeを検討)",
367 }
368}
369
370fn refactoring_action_japanese(action: &crate::balance::RefactoringAction) -> String {
372 use crate::balance::RefactoringAction;
373 match action {
374 RefactoringAction::IntroduceTrait { suggested_name, .. } => {
375 format!("トレイト `{}` を導入して抽象化する", suggested_name)
376 }
377 RefactoringAction::MoveCloser { target_location } => {
378 format!("`{}` に移動して距離を縮める", target_location)
379 }
380 RefactoringAction::ExtractAdapter { adapter_name, .. } => {
381 format!("アダプタ `{}` を抽出する", adapter_name)
382 }
383 RefactoringAction::SplitModule { suggested_modules } => {
384 format!("モジュールを分割: {}", suggested_modules.join(", "))
385 }
386 RefactoringAction::SimplifyAbstraction { .. } => "抽象化を簡素化する".to_string(),
387 RefactoringAction::BreakCycle {
388 suggested_direction,
389 } => {
390 format!("循環を断つ: {}", suggested_direction)
391 }
392 RefactoringAction::StabilizeInterface { interface_name } => {
393 format!("安定したインターフェース `{}` を追加", interface_name)
394 }
395 RefactoringAction::General { action } => action.clone(),
396 RefactoringAction::AddGetters { .. } => "getterメソッドを追加する".to_string(),
397 RefactoringAction::IntroduceNewtype {
398 suggested_name,
399 wrapped_type,
400 } => {
401 format!(
402 "newtype `struct {}({})` を導入",
403 suggested_name, wrapped_type
404 )
405 }
406 }
407}
408
409pub fn generate_report<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
411 generate_report_with_thresholds(metrics, &IssueThresholds::default(), writer)
412}
413
414pub fn generate_report_with_thresholds<W: Write>(
416 metrics: &ProjectMetrics,
417 thresholds: &IssueThresholds,
418 writer: &mut W,
419) -> io::Result<()> {
420 let report = analyze_project_balance_with_thresholds(metrics, thresholds);
421
422 writeln!(writer, "# Coupling Analysis Report\n")?;
423
424 write_executive_summary(metrics, &report, writer)?;
426
427 if !report.issues.is_empty() {
429 write_refactoring_priorities(&report, writer)?;
430 }
431
432 write_issues_by_type(&report, writer)?;
434
435 write_coupling_section(metrics, writer)?;
437
438 write_module_section(metrics, writer)?;
440
441 write_volatility_section(metrics, writer)?;
443
444 write_circular_dependencies_section(metrics, writer)?;
446
447 write_best_practices(writer)?;
449
450 Ok(())
451}
452
453fn write_executive_summary<W: Write>(
454 metrics: &ProjectMetrics,
455 report: &ProjectBalanceReport,
456 writer: &mut W,
457) -> io::Result<()> {
458 writeln!(writer, "## Executive Summary\n")?;
459
460 let grade_emoji = match report.health_grade {
462 crate::balance::HealthGrade::S => "⚠️",
463 crate::balance::HealthGrade::A => "🟢",
464 crate::balance::HealthGrade::B => "🟢",
465 crate::balance::HealthGrade::C => "🟡",
466 crate::balance::HealthGrade::D => "🟠",
467 crate::balance::HealthGrade::F => "🔴",
468 };
469
470 writeln!(
471 writer,
472 "**Health Grade**: {} {}\n",
473 grade_emoji, report.health_grade
474 )?;
475
476 writeln!(writer, "| Metric | Value |")?;
477 writeln!(writer, "|--------|-------|")?;
478 writeln!(writer, "| Files Analyzed | {} |", metrics.total_files)?;
479 writeln!(writer, "| Total Modules | {} |", metrics.module_count())?;
480 writeln!(writer, "| Total Couplings | {} |", report.total_couplings)?;
481 writeln!(
482 writer,
483 "| Balance Score | {:.2}/1.00 |",
484 report.average_score
485 )?;
486 writeln!(
487 writer,
488 "| Balanced | {} ({:.0}%) |",
489 report.balanced_count,
490 if report.total_couplings > 0 {
491 (report.balanced_count as f64 / report.total_couplings as f64) * 100.0
492 } else {
493 100.0
494 }
495 )?;
496 writeln!(
497 writer,
498 "| Needs Refactoring | {} |",
499 report.needs_refactoring
500 )?;
501 writeln!(writer)?;
502
503 let critical = *report
505 .issues_by_severity
506 .get(&Severity::Critical)
507 .unwrap_or(&0);
508 let high = *report.issues_by_severity.get(&Severity::High).unwrap_or(&0);
509 let medium = *report
510 .issues_by_severity
511 .get(&Severity::Medium)
512 .unwrap_or(&0);
513 let low = *report.issues_by_severity.get(&Severity::Low).unwrap_or(&0);
514
515 if critical > 0 || high > 0 {
516 writeln!(writer, "**⚠️ Action Required**\n")?;
517 if critical > 0 {
518 writeln!(
519 writer,
520 "- 🔴 **{} Critical** issues must be fixed immediately",
521 critical
522 )?;
523 }
524 if high > 0 {
525 writeln!(
526 writer,
527 "- 🟠 **{} High** priority issues should be addressed soon",
528 high
529 )?;
530 }
531 if medium > 0 {
532 writeln!(writer, "- 🟡 {} Medium priority issues to review", medium)?;
533 }
534 if low > 0 {
535 writeln!(writer, "- {} Low priority suggestions", low)?;
536 }
537 writeln!(writer)?;
538 } else if medium > 0 {
539 writeln!(
540 writer,
541 "**ℹ️ Review Suggested**: {} issues to consider.\n",
542 medium + low
543 )?;
544 } else {
545 writeln!(
546 writer,
547 "**✅ Good Health**: No significant coupling issues detected.\n"
548 )?;
549 }
550
551 Ok(())
552}
553
554fn write_refactoring_priorities<W: Write>(
555 report: &ProjectBalanceReport,
556 writer: &mut W,
557) -> io::Result<()> {
558 writeln!(writer, "## 🔧 Refactoring Priorities\n")?;
559
560 writeln!(writer, "### Immediate Actions\n")?;
562
563 let priority_issues: Vec<_> = report
564 .issues
565 .iter()
566 .filter(|i| i.severity >= Severity::Medium)
567 .take(5)
568 .collect();
569
570 if priority_issues.is_empty() {
571 writeln!(writer, "No immediate refactoring actions required.\n")?;
572 return Ok(());
573 }
574
575 for (i, issue) in priority_issues.iter().enumerate() {
576 let severity_icon = match issue.severity {
577 Severity::Critical => "🔴",
578 Severity::High => "🟠",
579 Severity::Medium => "🟡",
580 Severity::Low => "⚪",
581 };
582
583 writeln!(
584 writer,
585 "**{}. {} `{}` → `{}`**\n",
586 i + 1,
587 severity_icon,
588 issue.source,
589 issue.target
590 )?;
591
592 writeln!(
593 writer,
594 "- **Issue**: {} - {}",
595 issue.issue_type, issue.description
596 )?;
597 writeln!(writer, "- **Why**: {}", issue.issue_type.description())?;
598 writeln!(writer, "- **Action**: {}", issue.refactoring)?;
599 writeln!(writer, "- **Balance Score**: {:.2}\n", issue.balance_score)?;
600 }
601
602 Ok(())
603}
604
605fn write_issues_by_type<W: Write>(report: &ProjectBalanceReport, writer: &mut W) -> io::Result<()> {
606 if report.issues.is_empty() {
607 return Ok(());
608 }
609
610 writeln!(writer, "## Issues by Category\n")?;
611
612 let grouped = report.issues_grouped_by_type();
613
614 let mut issue_types: Vec<_> = grouped.keys().collect();
616 issue_types.sort_by(|a, b| {
617 let a_max = grouped
618 .get(a)
619 .and_then(|v| v.iter().map(|i| i.severity).max());
620 let b_max = grouped
621 .get(b)
622 .and_then(|v| v.iter().map(|i| i.severity).max());
623 b_max.cmp(&a_max)
624 });
625
626 for issue_type in issue_types {
627 if let Some(issues) = grouped.get(issue_type) {
628 let count = issues.len();
629
630 writeln!(writer, "### {} ({} instances)\n", issue_type, count)?;
631 writeln!(writer, "> {}\n", issue_type.description())?;
632
633 writeln!(writer, "| Severity | Source | Target | Action |")?;
635 writeln!(writer, "|----------|--------|--------|--------|")?;
636
637 for issue in issues.iter().take(5) {
638 let action_short = format!("{}", issue.refactoring);
639 let action_truncated = if action_short.len() > 40 {
640 format!("{}...", &action_short[..40])
641 } else {
642 action_short
643 };
644 writeln!(
645 writer,
646 "| {} | `{}` | `{}` | {} |",
647 issue.severity,
648 truncate_path(&issue.source, 25),
649 truncate_path(&issue.target, 25),
650 action_truncated
651 )?;
652 }
653
654 if count > 5 {
655 writeln!(writer, "\n*...and {} more instances*", count - 5)?;
656 }
657 writeln!(writer)?;
658 }
659 }
660
661 Ok(())
662}
663
664fn write_coupling_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
665 if metrics.couplings.is_empty() {
666 return Ok(());
667 }
668
669 writeln!(writer, "## Coupling Distribution\n")?;
670
671 writeln!(writer, "### By Integration Strength\n")?;
673 writeln!(writer, "| Strength | Count | % | Description |")?;
674 writeln!(writer, "|----------|-------|---|-------------|")?;
675
676 let total = metrics.couplings.len() as f64;
677 for (strength, label, desc) in [
678 (
679 IntegrationStrength::Contract,
680 "Contract",
681 "Depends on traits/interfaces only",
682 ),
683 (
684 IntegrationStrength::Model,
685 "Model",
686 "Uses data types/structs",
687 ),
688 (
689 IntegrationStrength::Functional,
690 "Functional",
691 "Calls specific functions",
692 ),
693 (
694 IntegrationStrength::Intrusive,
695 "Intrusive",
696 "Accesses internal details",
697 ),
698 ] {
699 let count = metrics
700 .couplings
701 .iter()
702 .filter(|c| c.strength == strength)
703 .count();
704 let pct = (count as f64 / total) * 100.0;
705 writeln!(writer, "| {} | {} | {:.0}% | {} |", label, count, pct, desc)?;
706 }
707 writeln!(writer)?;
708
709 writeln!(writer, "### By Distance\n")?;
711 writeln!(writer, "| Distance | Count | % |")?;
712 writeln!(writer, "|----------|-------|---|")?;
713
714 for (distance, label) in [
715 (Distance::SameModule, "Same Module (close)"),
716 (Distance::DifferentModule, "Different Module"),
717 (Distance::DifferentCrate, "External Crate (far)"),
718 ] {
719 let count = metrics
720 .couplings
721 .iter()
722 .filter(|c| c.distance == distance)
723 .count();
724 let pct = (count as f64 / total) * 100.0;
725 writeln!(writer, "| {} | {} | {:.0}% |", label, count, pct)?;
726 }
727 writeln!(writer)?;
728
729 let internal_couplings: Vec<_> = metrics
731 .couplings
732 .iter()
733 .filter(|c| c.distance != Distance::DifferentCrate)
734 .collect();
735
736 if !internal_couplings.is_empty() {
737 let internal_total = internal_couplings.len() as f64;
738 writeln!(writer, "### By Volatility (Internal Couplings)\n")?;
739 writeln!(writer, "| Volatility | Count | % | Impact on Balance |")?;
740 writeln!(writer, "|------------|-------|---|-------------------|")?;
741
742 for (volatility, label, impact) in [
743 (
744 crate::metrics::Volatility::Low,
745 "Low (rarely changes)",
746 "No penalty",
747 ),
748 (
749 crate::metrics::Volatility::Medium,
750 "Medium (sometimes changes)",
751 "Moderate penalty",
752 ),
753 (
754 crate::metrics::Volatility::High,
755 "High (frequently changes)",
756 "Significant penalty",
757 ),
758 ] {
759 let count = internal_couplings
760 .iter()
761 .filter(|c| c.volatility == volatility)
762 .count();
763 let pct = (count as f64 / internal_total) * 100.0;
764 writeln!(
765 writer,
766 "| {} | {} | {:.0}% | {} |",
767 label, count, pct, impact
768 )?;
769 }
770 writeln!(writer)?;
771 }
772
773 writeln!(writer, "### Worst Balanced Couplings\n")?;
775
776 let mut couplings_with_scores: Vec<_> = metrics
777 .couplings
778 .iter()
779 .map(|c| (c, BalanceScore::calculate(c)))
780 .collect();
781
782 couplings_with_scores.sort_by(|a, b| a.1.score.partial_cmp(&b.1.score).unwrap());
783
784 writeln!(
785 writer,
786 "| Source | Target | Strength | Distance | Volatility | Score | Status |"
787 )?;
788 writeln!(
789 writer,
790 "|--------|--------|----------|----------|------------|-------|--------|"
791 )?;
792
793 for (coupling, score) in couplings_with_scores.iter().take(15) {
794 let strength_str = match coupling.strength {
795 IntegrationStrength::Contract => "Contract",
796 IntegrationStrength::Model => "Model",
797 IntegrationStrength::Functional => "Functional",
798 IntegrationStrength::Intrusive => "Intrusive",
799 };
800 let distance_str = match coupling.distance {
801 Distance::SameFunction => "Same Fn",
802 Distance::SameModule => "Same Mod",
803 Distance::DifferentModule => "Diff Mod",
804 Distance::DifferentCrate => "External",
805 };
806 let volatility_str = match coupling.volatility {
807 crate::metrics::Volatility::Low => "Low",
808 crate::metrics::Volatility::Medium => "Med",
809 crate::metrics::Volatility::High => "High",
810 };
811 let status = match score.interpretation {
812 crate::balance::BalanceInterpretation::Balanced => "✅ Balanced",
813 crate::balance::BalanceInterpretation::Acceptable => "✅ OK",
814 crate::balance::BalanceInterpretation::NeedsReview => "🟡 Review",
815 crate::balance::BalanceInterpretation::NeedsRefactoring => "🟠 Refactor",
816 crate::balance::BalanceInterpretation::Critical => "🔴 Critical",
817 };
818
819 writeln!(
820 writer,
821 "| `{}` | `{}` | {} | {} | {} | {:.2} | {} |",
822 truncate_path(&coupling.source, 20),
823 truncate_path(&coupling.target, 20),
824 strength_str,
825 distance_str,
826 volatility_str,
827 score.score,
828 status
829 )?;
830 }
831
832 if couplings_with_scores.len() > 15 {
833 writeln!(
834 writer,
835 "\n*Showing 15 of {} couplings*",
836 couplings_with_scores.len()
837 )?;
838 }
839 writeln!(writer)?;
840
841 Ok(())
842}
843
844fn write_module_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
845 if metrics.modules.is_empty() {
846 return Ok(());
847 }
848
849 writeln!(writer, "## Module Statistics\n")?;
850
851 writeln!(
852 writer,
853 "| Module | Trait Impl | Inherent Impl | Internal Deps | External Deps |"
854 )?;
855 writeln!(
856 writer,
857 "|--------|------------|---------------|---------------|---------------|"
858 )?;
859
860 let mut modules: Vec<_> = metrics.modules.iter().collect();
861 modules.sort_by(|a, b| {
862 let a_deps = a.1.internal_deps.len() + a.1.external_deps.len();
863 let b_deps = b.1.internal_deps.len() + b.1.external_deps.len();
864 b_deps.cmp(&a_deps)
865 });
866
867 for (name, module) in modules.iter().take(20) {
868 writeln!(
869 writer,
870 "| `{}` | {} | {} | {} | {} |",
871 truncate_path(name, 30),
872 module.trait_impl_count,
873 module.inherent_impl_count,
874 module.internal_deps.len(),
875 module.external_deps.len()
876 )?;
877 }
878
879 if modules.len() > 20 {
880 writeln!(writer, "\n*Showing top 20 of {} modules*", modules.len())?;
881 }
882 writeln!(writer)?;
883
884 Ok(())
885}
886
887fn write_volatility_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
888 writeln!(writer, "## Volatility Analysis\n")?;
889
890 if metrics.file_changes.is_empty() {
891 writeln!(
892 writer,
893 "*Git history analysis not available. Run in a git repository for volatility data.*\n"
894 )?;
895 return Ok(());
896 }
897
898 let mut high_vol: Vec<_> = metrics
899 .file_changes
900 .iter()
901 .filter(|&(_, count)| *count > 10)
902 .collect();
903
904 high_vol.sort_by(|a, b| b.1.cmp(a.1));
905
906 if high_vol.is_empty() {
907 writeln!(
908 writer,
909 "No high volatility files detected (threshold: >10 changes).\n"
910 )?;
911 } else {
912 writeln!(writer, "### High Volatility Files\n")?;
913 writeln!(
914 writer,
915 "⚠️ Strong coupling to these files increases cascading change risk.\n"
916 )?;
917 writeln!(writer, "| File | Changes |")?;
918 writeln!(writer, "|------|---------|")?;
919 for (file, count) in high_vol.iter().take(10) {
920 writeln!(writer, "| `{}` | {} |", file, count)?;
921 }
922 writeln!(writer)?;
923 }
924
925 Ok(())
926}
927
928fn write_circular_dependencies_section<W: Write>(
929 metrics: &ProjectMetrics,
930 writer: &mut W,
931) -> io::Result<()> {
932 let summary = metrics.circular_dependency_summary();
933
934 if summary.total_cycles == 0 {
935 writeln!(writer, "## Circular Dependencies\n")?;
936 writeln!(writer, "✅ No circular dependencies detected.\n")?;
937 return Ok(());
938 }
939
940 writeln!(writer, "## ⚠️ Circular Dependencies\n")?;
941 writeln!(
942 writer,
943 "Found **{} circular dependency cycle(s)** involving **{} modules**.\n",
944 summary.total_cycles, summary.affected_modules
945 )?;
946
947 writeln!(
948 writer,
949 "Circular dependencies make code harder to understand, test, and maintain."
950 )?;
951 writeln!(writer, "Consider breaking cycles by:\n")?;
952 writeln!(writer, "1. Extracting shared types into a separate module")?;
953 writeln!(writer, "2. Inverting dependencies using traits/interfaces")?;
954 writeln!(writer, "3. Moving functionality to reduce coupling\n")?;
955
956 writeln!(writer, "### Detected Cycles\n")?;
957
958 for (i, cycle) in summary.cycles.iter().take(10).enumerate() {
959 let cycle_str = cycle.join(" → ");
960 writeln!(
961 writer,
962 "{}. `{}` → `{}`",
963 i + 1,
964 cycle_str,
965 cycle.first().unwrap_or(&"?".to_string())
966 )?;
967 }
968
969 if summary.cycles.len() > 10 {
970 writeln!(
971 writer,
972 "\n*...and {} more cycles*",
973 summary.cycles.len() - 10
974 )?;
975 }
976 writeln!(writer)?;
977
978 Ok(())
979}
980
981fn write_best_practices<W: Write>(writer: &mut W) -> io::Result<()> {
982 writeln!(writer, "## Balance Guidelines\n")?;
983
984 writeln!(
985 writer,
986 "The goal is **balanced coupling**, not zero coupling.\n"
987 )?;
988
989 writeln!(writer, "### Ideal Patterns ✅\n")?;
990 writeln!(writer, "| Pattern | Example | Why It Works |")?;
991 writeln!(writer, "|---------|---------|--------------|")?;
992 writeln!(
993 writer,
994 "| Strong + Close | `impl` blocks in same module | Cohesion within boundaries |"
995 )?;
996 writeln!(
997 writer,
998 "| Weak + Far | Trait impl for external crate | Loose coupling across boundaries |"
999 )?;
1000 writeln!(writer)?;
1001
1002 writeln!(writer, "### Problematic Patterns ❌\n")?;
1003 writeln!(writer, "| Pattern | Problem | Solution |")?;
1004 writeln!(writer, "|---------|---------|----------|")?;
1005 writeln!(
1006 writer,
1007 "| Strong + Far | Global complexity | Introduce adapter or move closer |"
1008 )?;
1009 writeln!(
1010 writer,
1011 "| Strong + Volatile | Cascading changes | Add stable interface |"
1012 )?;
1013 writeln!(
1014 writer,
1015 "| Intrusive + Cross-boundary | Encapsulation violation | Extract trait API |"
1016 )?;
1017 writeln!(writer)?;
1018
1019 Ok(())
1020}
1021
1022fn truncate_path(path: &str, max_len: usize) -> String {
1023 if path.len() <= max_len {
1024 path.to_string()
1025 } else {
1026 format!("...{}", &path[path.len() - max_len + 3..])
1027 }
1028}
1029
1030pub fn generate_ai_output<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
1037 generate_ai_output_with_thresholds(metrics, &IssueThresholds::default(), writer)
1038}
1039
1040pub fn generate_ai_output_with_thresholds<W: Write>(
1042 metrics: &ProjectMetrics,
1043 thresholds: &IssueThresholds,
1044 writer: &mut W,
1045) -> io::Result<()> {
1046 let report = analyze_project_balance_with_thresholds(metrics, thresholds);
1047
1048 let project_name = metrics.workspace_name.as_deref().unwrap_or("project");
1049 writeln!(writer, "Coupling Issues in {}:", project_name)?;
1050 writeln!(
1051 writer,
1052 "────────────────────────────────────────────────────────────"
1053 )?;
1054 writeln!(writer)?;
1055
1056 writeln!(
1058 writer,
1059 "Grade: {} | Score: {:.2} | Issues: {} High, {} Medium",
1060 report.health_grade,
1061 report.average_score,
1062 report.issues_by_severity.get(&Severity::High).unwrap_or(&0),
1063 report
1064 .issues_by_severity
1065 .get(&Severity::Medium)
1066 .unwrap_or(&0)
1067 )?;
1068 writeln!(writer)?;
1069
1070 if report.issues.is_empty() {
1072 writeln!(writer, "✅ No coupling issues detected.")?;
1073 writeln!(writer)?;
1074 } else {
1075 writeln!(writer, "Issues:")?;
1076 writeln!(writer)?;
1077
1078 for (i, issue) in report.issues.iter().take(10).enumerate() {
1079 let severity_marker = match issue.severity {
1080 Severity::Critical => "🔴",
1081 Severity::High => "🟠",
1082 Severity::Medium => "🟡",
1083 Severity::Low => "⚪",
1084 };
1085
1086 writeln!(
1087 writer,
1088 "{}. {} {} → {}",
1089 i + 1,
1090 severity_marker,
1091 issue.source,
1092 issue.target
1093 )?;
1094 writeln!(writer, " Type: {}", issue.issue_type)?;
1095 writeln!(writer, " Problem: {}", issue.description)?;
1096 writeln!(writer, " Fix: {}", issue.refactoring)?;
1097 writeln!(writer)?;
1098 }
1099
1100 if report.issues.len() > 10 {
1101 writeln!(writer, "... and {} more issues", report.issues.len() - 10)?;
1102 writeln!(writer)?;
1103 }
1104 }
1105
1106 let circular = metrics.circular_dependency_summary();
1108 if circular.total_cycles > 0 {
1109 writeln!(
1110 writer,
1111 "Circular Dependencies ({} cycles):",
1112 circular.total_cycles
1113 )?;
1114 for cycle in circular.cycles.iter().take(5) {
1115 writeln!(
1116 writer,
1117 " {} → {}",
1118 cycle.join(" → "),
1119 cycle.first().unwrap_or(&"?".to_string())
1120 )?;
1121 }
1122 writeln!(writer)?;
1123 }
1124
1125 if !report.issues.is_empty() || circular.total_cycles > 0 {
1127 writeln!(
1128 writer,
1129 "────────────────────────────────────────────────────────────"
1130 )?;
1131 writeln!(writer)?;
1132
1133 writeln!(
1135 writer,
1136 "💡 To refactor with AI, copy this output and use this prompt:"
1137 )?;
1138 writeln!(writer)?;
1139 writeln!(writer, "```")?;
1140 writeln!(
1141 writer,
1142 "Analyze the coupling issues above from `cargo coupling --ai`. "
1143 )?;
1144 writeln!(
1145 writer,
1146 "For each issue, suggest specific code changes to reduce coupling."
1147 )?;
1148 writeln!(
1149 writer,
1150 "Focus on introducing traits, moving code closer, or breaking circular dependencies."
1151 )?;
1152 writeln!(writer, "```")?;
1153 }
1154
1155 Ok(())
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161 use std::path::PathBuf;
1162
1163 #[test]
1164 fn test_generate_summary() {
1165 let metrics = ProjectMetrics::new();
1166 let mut output = Vec::new();
1167
1168 let result = generate_summary(&metrics, &mut output);
1169 assert!(result.is_ok());
1170
1171 let output_str = String::from_utf8(output).unwrap();
1172 assert!(output_str.contains("Balanced Coupling Analysis"));
1173 assert!(output_str.contains("Grade:"));
1174 }
1175
1176 #[test]
1177 fn test_generate_report() {
1178 let metrics = ProjectMetrics::new();
1179 let mut output = Vec::new();
1180
1181 let result = generate_report(&metrics, &mut output);
1182 assert!(result.is_ok());
1183
1184 let output_str = String::from_utf8(output).unwrap();
1185 assert!(output_str.contains("# Coupling Analysis Report"));
1186 assert!(output_str.contains("Executive Summary"));
1187 }
1188
1189 #[test]
1190 fn test_generate_report_with_modules() {
1191 use crate::metrics::ModuleMetrics;
1192
1193 let mut metrics = ProjectMetrics::new();
1194 let mut module = ModuleMetrics::new(PathBuf::from("lib.rs"), "lib".to_string());
1195 module.trait_impl_count = 3;
1196 module.inherent_impl_count = 2;
1197 metrics.add_module(module);
1198
1199 let mut output = Vec::new();
1200 let result = generate_report(&metrics, &mut output);
1201 assert!(result.is_ok());
1202
1203 let output_str = String::from_utf8(output).unwrap();
1204 assert!(output_str.contains("Module Statistics"));
1205 }
1206
1207 #[test]
1208 fn test_truncate_path() {
1209 assert_eq!(truncate_path("short", 10), "short");
1210 assert_eq!(
1211 truncate_path("this_is_a_very_long_path", 15),
1212 "...ry_long_path"
1213 );
1214 }
1215}