Skip to main content

cargo_coupling/
report.rs

1//! Report generation for coupling analysis
2//!
3//! Generates human-readable reports with actionable refactoring suggestions.
4
5use 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
13/// Generate a summary report to the given writer
14pub fn generate_summary<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
15    generate_summary_with_thresholds(metrics, &IssueThresholds::default(), writer)
16}
17
18/// Generate a summary report with custom thresholds
19pub 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    // 3-Dimensional Analysis
56    if !metrics.couplings.is_empty() {
57        // Strength distribution
58        let (intr_pct, func_pct, model_pct, contract_pct) = dimension_stats.strength_percentages();
59        // Distance distribution
60        let (same_pct, diff_pct, ext_pct) = dimension_stats.distance_percentages();
61        // Volatility distribution
62        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        // Balance Classification
106        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    // Issue breakdown
202    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    // Top priority if any
256    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    // Rust Design Quality (newtype usage)
282    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    // Circular dependencies
317    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    // Design decision guide (Japanese only, for educational purposes)
335    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
350/// Get Japanese translation for issue type
351fn 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
370/// Get Japanese translation for refactoring action
371fn 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
409/// Generate a full Markdown report with refactoring suggestions
410pub fn generate_report<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
411    generate_report_with_thresholds(metrics, &IssueThresholds::default(), writer)
412}
413
414/// Generate a full Markdown report with custom thresholds
415pub 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    // Executive Summary
425    write_executive_summary(metrics, &report, writer)?;
426
427    // Refactoring Priorities (if any issues)
428    if !report.issues.is_empty() {
429        write_refactoring_priorities(&report, writer)?;
430    }
431
432    // Detailed Issues by Type
433    write_issues_by_type(&report, writer)?;
434
435    // Coupling details
436    write_coupling_section(metrics, writer)?;
437
438    // Module analysis
439    write_module_section(metrics, writer)?;
440
441    // Volatility section
442    write_volatility_section(metrics, writer)?;
443
444    // Circular dependency section
445    write_circular_dependencies_section(metrics, writer)?;
446
447    // Best practices
448    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    // Health Grade with emoji
461    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    // Issue counts
504    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    // Show top 5 priority issues with concrete actions
561    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    // Order by severity of issues in each group
615    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            // Show up to 5 examples
634            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    // Strength distribution
672    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    // Distance distribution
710    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    // Volatility distribution (only for internal couplings where we have git data)
730    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    // Worst balanced couplings
774    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
1030/// Generate AI-friendly output format for coding agents
1031///
1032/// This format is designed to be:
1033/// 1. Concise and structured for LLM consumption
1034/// 2. Actionable with specific file/module references
1035/// 3. Copy-paste ready for AI refactoring prompts
1036pub 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
1040/// Generate AI-friendly output with custom thresholds
1041pub 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    // Summary line
1057    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    // List issues in a structured format
1071    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    // Circular dependencies (critical for AI to understand)
1107    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    // only show ai refactor advice if there is something to refactor
1126    if !report.issues.is_empty() || circular.total_cycles > 0 {
1127        writeln!(
1128            writer,
1129            "────────────────────────────────────────────────────────────"
1130        )?;
1131        writeln!(writer)?;
1132
1133        // AI prompt suggestion
1134        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}