Skip to main content

cargo_coupling/
report.rs

1//! Text and Markdown report rendering for coupling analysis.
2//!
3//! This module translates balance scores, issue lists, volatility signals, and
4//! blind-spot manifests into CLI-facing summaries and full reports.
5
6use std::io::{self, Write};
7
8use crate::balance::action::RefactoringAction;
9use crate::balance::coupling::is_crate_root_facade;
10use crate::balance::grade::{HealthGrade, ProjectBalanceReport};
11use crate::balance::issue::CouplingIssue;
12use crate::balance::issue_type::IssueType;
13use crate::balance::project::analyze_project_balance_with_thresholds;
14use crate::balance::score::{BalanceInterpretation, BalanceScore, IssueThresholds};
15use crate::balance::severity::Severity;
16use crate::manifest::{AnalysisManifest, ManifestContext, build_manifest};
17use crate::metrics::dimensions::{Distance, IntegrationStrength};
18use crate::metrics::project::ProjectMetrics;
19
20const DEFAULT_STRONG_TEMPORAL_LIMIT: usize = 5;
21
22// ===== Report Options =====
23
24/// Options for the default human-readable text report.
25#[derive(Debug, Clone, Copy, Default)]
26pub struct TextReportOptions {
27    /// Include the full structural blind-spot descriptions instead of a pointer.
28    pub show_structural_blind_spots: bool,
29    /// Include all temporal-coupling pairs instead of the concise default.
30    pub show_all_temporal_couplings: bool,
31}
32
33// ===== Summary Report =====
34
35/// Generate a summary report to the given writer
36pub fn generate_summary<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
37    let manifest = default_manifest();
38    generate_summary_with_thresholds(metrics, &IssueThresholds::default(), &manifest, writer)
39}
40
41/// Generate a summary report with custom thresholds
42pub fn generate_summary_with_thresholds<W: Write>(
43    metrics: &ProjectMetrics,
44    thresholds: &IssueThresholds,
45    manifest: &AnalysisManifest,
46    writer: &mut W,
47) -> io::Result<()> {
48    generate_summary_with_options(metrics, thresholds, manifest, false, writer)
49}
50
51/// Generate a summary report with custom thresholds and blind-spot detail.
52pub fn generate_summary_with_options<W: Write>(
53    metrics: &ProjectMetrics,
54    thresholds: &IssueThresholds,
55    manifest: &AnalysisManifest,
56    show_structural_blind_spots: bool,
57    writer: &mut W,
58) -> io::Result<()> {
59    let report = analyze_project_balance_with_thresholds(metrics, thresholds);
60    let dimension_stats = metrics.calculate_dimension_stats();
61    let jp = thresholds.japanese;
62
63    let project_name = metrics.workspace_name.as_deref().unwrap_or("project");
64
65    if jp {
66        writeln!(writer, "カップリング分析: {}", project_name)?;
67        writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
68        writeln!(writer)?;
69        writeln!(
70            writer,
71            "評価: {} | スコア: {:.2}/1.00 | モジュール数: {}",
72            report.health_grade,
73            report.average_score,
74            metrics.module_count()
75        )?;
76        writeln!(writer, "理由: {}", report.grade_rationale.summary)?;
77    } else {
78        writeln!(writer, "Balanced Coupling Analysis: {}", project_name)?;
79        writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
80        writeln!(writer)?;
81        writeln!(
82            writer,
83            "Grade: {} | Score: {:.2}/1.00 | Modules: {}",
84            report.health_grade,
85            report.average_score,
86            metrics.module_count()
87        )?;
88        writeln!(writer, "Why this grade: {}", report.grade_rationale.summary)?;
89    }
90    writeln!(writer)?;
91
92    // 3-Dimensional Analysis
93    if !metrics.couplings.is_empty() {
94        // Strength distribution
95        let (intr_pct, func_pct, model_pct, contract_pct) = dimension_stats.strength_percentages();
96        // Distance distribution
97        let (same_pct, diff_pct, ext_pct) = dimension_stats.distance_percentages();
98        // Volatility distribution
99        let (low_pct, med_pct, high_pct) = dimension_stats.volatility_percentages();
100
101        if jp {
102            writeln!(writer, "3次元分析:")?;
103            writeln!(
104                writer,
105                "  結合強度: Contract {:.0}% / Model {:.0}% / Functional {:.0}% / Intrusive {:.0}%",
106                contract_pct, model_pct, func_pct, intr_pct
107            )?;
108            writeln!(
109                writer,
110                "           (トレイト)   (型)      (関数)        (内部アクセス)"
111            )?;
112            writeln!(
113                writer,
114                "  距離:     同一モジュール {:.0}% / 別モジュール {:.0}% / 外部 {:.0}%",
115                same_pct, diff_pct, ext_pct
116            )?;
117            writeln!(
118                writer,
119                "  変更頻度: 低 {:.0}% / 中 {:.0}% / 高 {:.0}%",
120                low_pct, med_pct, high_pct
121            )?;
122        } else {
123            writeln!(writer, "3-Dimensional Analysis:")?;
124            writeln!(
125                writer,
126                "  Strength:   Contract {:.0}% / Model {:.0}% / Functional {:.0}% / Intrusive {:.0}%",
127                contract_pct, model_pct, func_pct, intr_pct
128            )?;
129            writeln!(
130                writer,
131                "  Distance:   Same {:.0}% / Different {:.0}% / External {:.0}%",
132                same_pct, diff_pct, ext_pct
133            )?;
134            writeln!(
135                writer,
136                "  Volatility: Low {:.0}% / Medium {:.0}% / High {:.0}%",
137                low_pct, med_pct, high_pct
138            )?;
139        }
140        writeln!(writer)?;
141
142        // Balance Classification
143        if jp {
144            writeln!(writer, "バランス状態:")?;
145        } else {
146            writeln!(writer, "Balance State:")?;
147        }
148        let bc = &dimension_stats.balance_counts;
149        let total = dimension_stats.total();
150        if bc.high_cohesion > 0 {
151            if jp {
152                writeln!(
153                    writer,
154                    "  ✅ 高凝集 (強い結合 + 近い距離): {} ({:.0}%) ← 理想的",
155                    bc.high_cohesion,
156                    bc.high_cohesion as f64 / total as f64 * 100.0
157                )?;
158            } else {
159                writeln!(
160                    writer,
161                    "  ✅ High Cohesion (strong+close): {} ({:.0}%)",
162                    bc.high_cohesion,
163                    bc.high_cohesion as f64 / total as f64 * 100.0
164                )?;
165            }
166        }
167        if bc.loose_coupling > 0 {
168            if jp {
169                writeln!(
170                    writer,
171                    "  ✅ 疎結合 (弱い結合 + 遠い距離): {} ({:.0}%) ← 理想的",
172                    bc.loose_coupling,
173                    bc.loose_coupling as f64 / total as f64 * 100.0
174                )?;
175            } else {
176                writeln!(
177                    writer,
178                    "  ✅ Loose Coupling (weak+far): {} ({:.0}%)",
179                    bc.loose_coupling,
180                    bc.loose_coupling as f64 / total as f64 * 100.0
181                )?;
182            }
183        }
184        if bc.acceptable > 0 {
185            if jp {
186                writeln!(
187                    writer,
188                    "  🤔 許容可能 (強い結合 + 遠い距離 + 安定): {} ({:.0}%)",
189                    bc.acceptable,
190                    bc.acceptable as f64 / total as f64 * 100.0
191                )?;
192            } else {
193                writeln!(
194                    writer,
195                    "  🤔 Acceptable (strong+far+stable): {} ({:.0}%)",
196                    bc.acceptable,
197                    bc.acceptable as f64 / total as f64 * 100.0
198                )?;
199            }
200        }
201        if bc.pain > 0 {
202            if jp {
203                writeln!(
204                    writer,
205                    "  ❌ ペインゾーン (強い結合 + 遠い距離 + 頻繁に変更): {} ({:.0}%)",
206                    bc.pain,
207                    bc.pain as f64 / total as f64 * 100.0
208                )?;
209            } else {
210                writeln!(
211                    writer,
212                    "  ❌ Pain Zone (strong+far+volatile): {} ({:.0}%)",
213                    bc.pain,
214                    bc.pain as f64 / total as f64 * 100.0
215                )?;
216            }
217        }
218        if bc.local_complexity > 0 {
219            if jp {
220                writeln!(
221                    writer,
222                    "  🔍 局所的複雑性 (弱い結合 + 近い距離): {} ({:.0}%)",
223                    bc.local_complexity,
224                    bc.local_complexity as f64 / total as f64 * 100.0
225                )?;
226            } else {
227                writeln!(
228                    writer,
229                    "  🔍 Local Complexity (weak+close): {} ({:.0}%)",
230                    bc.local_complexity,
231                    bc.local_complexity as f64 / total as f64 * 100.0
232                )?;
233            }
234        }
235        writeln!(writer)?;
236    }
237
238    // Issue breakdown
239    let critical = *report
240        .issues_by_severity
241        .get(&Severity::Critical)
242        .unwrap_or(&0);
243    let high = *report.issues_by_severity.get(&Severity::High).unwrap_or(&0);
244    let medium = *report
245        .issues_by_severity
246        .get(&Severity::Medium)
247        .unwrap_or(&0);
248    let low = *report.issues_by_severity.get(&Severity::Low).unwrap_or(&0);
249
250    if critical > 0 || high > 0 || medium > 0 || low > 0 {
251        if jp {
252            writeln!(writer, "検出された問題:")?;
253            if critical > 0 {
254                writeln!(writer, "  🔴 緊急: {} 件 (すぐに修正が必要)", critical)?;
255            }
256            if high > 0 {
257                writeln!(writer, "  🟠 高: {} 件 (早めに対処)", high)?;
258            }
259            if medium > 0 {
260                writeln!(writer, "  🟡 中: {} 件", medium)?;
261            }
262            if low > 0 {
263                writeln!(writer, "  ⚪ 低: {} 件", low)?;
264            }
265        } else {
266            writeln!(writer, "Detected Issues:")?;
267            if critical > 0 {
268                writeln!(writer, "  🔴 Critical: {} (must fix)", critical)?;
269            }
270            if high > 0 {
271                writeln!(writer, "  🟠 High: {} (should fix)", high)?;
272            }
273            if medium > 0 {
274                writeln!(writer, "  🟡 Medium: {}", medium)?;
275            }
276            if low > 0 {
277                writeln!(writer, "  ⚪ Low: {}", low)?;
278            }
279        }
280        writeln!(writer)?;
281    } else if thresholds.strict_mode {
282        if jp {
283            writeln!(writer, "検出された問題: なし (--all で低優先度も表示)\n")?;
284        } else {
285            writeln!(
286                writer,
287                "Detected Issues: None (use --all to see Low severity)\n"
288            )?;
289        }
290    }
291
292    // Top priority if any
293    if !report.top_priorities.is_empty() {
294        if jp {
295            writeln!(writer, "優先的に対処すべき問題:")?;
296            for issue in report.top_priorities.iter().take(3) {
297                let issue_jp = issue_type_japanese(issue.issue_type);
298                writeln!(writer, "  - {} | {}", issue_jp, issue.source)?;
299                writeln!(
300                    writer,
301                    "    → {}",
302                    refactoring_action_japanese(&issue.refactoring)
303                )?;
304            }
305        } else {
306            writeln!(writer, "Top Priorities:")?;
307            for issue in report.top_priorities.iter().take(3) {
308                writeln!(
309                    writer,
310                    "  - [{}] {} → {}",
311                    issue.severity, issue.source, issue.target
312                )?;
313            }
314        }
315        writeln!(writer)?;
316    }
317
318    // Rust Design Quality (newtype usage)
319    let newtype_count = metrics.total_newtype_count();
320    let type_count = metrics.total_type_count();
321    if type_count > 0 {
322        let newtype_ratio = metrics.newtype_ratio() * 100.0;
323        if jp {
324            let quality = if newtype_ratio >= 20.0 {
325                "✅ 良好"
326            } else if newtype_ratio >= 10.0 {
327                "🤔 増やすことを検討"
328            } else {
329                "⚠️ 少ない"
330            };
331            writeln!(
332                writer,
333                "Rustパターン: newtype使用率 {}/{} ({:.0}%) - {}",
334                newtype_count, type_count, newtype_ratio, quality
335            )?;
336        } else {
337            let quality = if newtype_ratio >= 20.0 {
338                "✅ Good"
339            } else if newtype_ratio >= 10.0 {
340                "🤔 Consider more"
341            } else {
342                "⚠️ Low usage"
343            };
344            writeln!(
345                writer,
346                "Rust Patterns: Newtype usage: {}/{} ({:.0}%) - {}",
347                newtype_count, type_count, newtype_ratio, quality
348            )?;
349        }
350        writeln!(writer)?;
351    }
352
353    // Circular dependencies
354    let circular = metrics.circular_dependency_summary();
355    if circular.total_cycles > 0 {
356        if jp {
357            writeln!(
358                writer,
359                "⚠️ 循環依存: {} サイクル ({} モジュール)",
360                circular.total_cycles, circular.affected_modules
361            )?;
362        } else {
363            writeln!(
364                writer,
365                "⚠️ Circular Dependencies: {} cycles ({} modules)",
366                circular.total_cycles, circular.affected_modules
367            )?;
368        }
369    }
370
371    // Design decision guide (Japanese only, for educational purposes)
372    if jp {
373        writeln!(writer)?;
374        writeln!(writer, "設計判断ガイド (Khononov):")?;
375        writeln!(writer, "  ✅ 強い結合 + 近い距離 → 高凝集 (理想的)")?;
376        writeln!(writer, "  ✅ 弱い結合 + 遠い距離 → 疎結合 (理想的)")?;
377        writeln!(writer, "  🤔 強い結合 + 遠い距離 + 安定 → 許容可能")?;
378        writeln!(
379            writer,
380            "  ❌ 強い結合 + 遠い距離 + 頻繁に変更 → 要リファクタリング"
381        )?;
382    }
383
384    write_manifest_summary_section(manifest, jp, show_structural_blind_spots, writer)?;
385
386    Ok(())
387}
388
389// ===== Localization Helpers =====
390
391/// Get Japanese translation for issue type
392fn issue_type_japanese(issue_type: IssueType) -> &'static str {
393    use IssueType;
394    match issue_type {
395        IssueType::GlobalComplexity => "グローバル複雑性 (遠距離への強い依存)",
396        IssueType::CascadingChangeRisk => "変更波及リスク (頻繁に変わるものへの依存)",
397        IssueType::InappropriateIntimacy => "不適切な親密さ (内部実装への依存)",
398        IssueType::HighEfferentCoupling => "出力依存過多 (多くのモジュールに依存)",
399        IssueType::HighAfferentCoupling => "入力依存過多 (多くのモジュールから依存される)",
400        IssueType::UnnecessaryAbstraction => "過剰な抽象化",
401        IssueType::CircularDependency => "循環依存",
402        IssueType::HiddenCoupling => "隠れた結合 (共変更のみで発見)",
403        IssueType::AccidentalVolatility => "偶発的な変更頻度",
404        IssueType::ScatteredExternalCoupling => "外部クレート結合の分散",
405        IssueType::ShallowModule => "浅いモジュール",
406        IssueType::PassThroughMethod => "パススルーメソッド",
407        IssueType::HighCognitiveLoad => "高認知負荷",
408        IssueType::GodModule => "神モジュール (責務が多すぎる)",
409        IssueType::PublicFieldExposure => "公開フィールド (getterを検討)",
410        IssueType::PrimitiveObsession => "プリミティブ過多 (newtypeを検討)",
411    }
412}
413
414/// Get Japanese translation for refactoring action
415fn refactoring_action_japanese(action: &RefactoringAction) -> String {
416    use RefactoringAction;
417    match action {
418        RefactoringAction::IntroduceTrait { suggested_name, .. } => {
419            format!("トレイト `{}` を導入して抽象化する", suggested_name)
420        }
421        RefactoringAction::MoveCloser { target_location } => {
422            format!("`{}` に移動して距離を縮める", target_location)
423        }
424        RefactoringAction::ExtractAdapter { adapter_name, .. } => {
425            format!("アダプタ `{}` を抽出する", adapter_name)
426        }
427        RefactoringAction::SplitModule { suggested_modules } => {
428            format!("モジュールを分割: {}", suggested_modules.join(", "))
429        }
430        RefactoringAction::SimplifyAbstraction { .. } => "抽象化を簡素化する".to_string(),
431        RefactoringAction::BreakCycle {
432            suggested_direction,
433        } => {
434            format!("循環を断つ: {}", suggested_direction)
435        }
436        RefactoringAction::StabilizeInterface { interface_name } => {
437            format!("安定したインターフェース `{}` を追加", interface_name)
438        }
439        RefactoringAction::General { action } => {
440            if action.starts_with("Introduce a `") && action.contains("` facade/wrapper module") {
441                let facade = action.split('`').nth(1).unwrap_or("facade");
442                format!("`{}` モジュールを導入し、直接利用をそこに集約する", facade)
443            } else if action == "Extract a shared abstraction or make the dependency explicit" {
444                "共有された抽象化を抽出するか、依存関係を明示する".to_string()
445            } else {
446                action.clone()
447            }
448        }
449        RefactoringAction::AddGetters { .. } => "getterメソッドを追加する".to_string(),
450        RefactoringAction::IntroduceNewtype {
451            suggested_name,
452            wrapped_type,
453        } => {
454            format!(
455                "newtype `struct {}({})` を導入",
456                suggested_name, wrapped_type
457            )
458        }
459    }
460}
461
462fn issue_instance_description_japanese(issue: &CouplingIssue) -> String {
463    use IssueType;
464    match issue.issue_type {
465        IssueType::HiddenCoupling => {
466            "明示的なコード依存はありませんが、ファイルが頻繁に一緒に変更されています。暗黙の知識や不足した抽象化を示している可能性があります。"
467                .to_string()
468        }
469        IssueType::AccidentalVolatility => {
470            "安定しているはずのサブドメインが頻繁に変更されています。本質的な業務変化ではなく、設計や所有権の問題によるチャーンの可能性があります。"
471                .to_string()
472        }
473        IssueType::ScatteredExternalCoupling => {
474            format!(
475                "{} は複数の内部モジュールから直接使われています。サードパーティ更新時のリスクがコードベース全体に広がっています。",
476                issue.target
477            )
478        }
479        _ => issue.description.clone(),
480    }
481}
482
483// ===== Full Markdown Report =====
484
485/// Generate a full Markdown report with refactoring suggestions
486pub fn generate_report<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
487    let manifest = default_manifest();
488    generate_report_with_thresholds(metrics, &IssueThresholds::default(), &manifest, writer)
489}
490
491/// Generate a full Markdown report with custom thresholds
492pub fn generate_report_with_thresholds<W: Write>(
493    metrics: &ProjectMetrics,
494    thresholds: &IssueThresholds,
495    manifest: &AnalysisManifest,
496    writer: &mut W,
497) -> io::Result<()> {
498    generate_report_with_options(
499        metrics,
500        thresholds,
501        manifest,
502        TextReportOptions::default(),
503        writer,
504    )
505}
506
507/// Generate a full Markdown report with custom thresholds and text options.
508pub fn generate_report_with_options<W: Write>(
509    metrics: &ProjectMetrics,
510    thresholds: &IssueThresholds,
511    manifest: &AnalysisManifest,
512    options: TextReportOptions,
513    writer: &mut W,
514) -> io::Result<()> {
515    let report = analyze_project_balance_with_thresholds(metrics, thresholds);
516
517    writeln!(writer, "# Coupling Analysis Report\n")?;
518
519    // Executive Summary
520    let jp = thresholds.japanese;
521    write_executive_summary(metrics, &report, jp, writer)?;
522
523    // Refactoring Priorities (if any issues)
524    if !report.issues.is_empty() {
525        write_refactoring_priorities(&report, jp, writer)?;
526    }
527
528    // Detailed Issues by Type
529    write_issues_by_type(&report, jp, writer)?;
530
531    // Coupling details
532    write_coupling_section(metrics, writer)?;
533
534    // Module analysis
535    write_module_section(metrics, writer)?;
536
537    // Volatility section
538    write_volatility_section(metrics, writer)?;
539
540    // Temporal coupling section
541    write_temporal_coupling_section(metrics, options.show_all_temporal_couplings, writer)?;
542
543    // Circular dependency section
544    write_circular_dependencies_section(metrics, writer)?;
545
546    // Best practices
547    write_best_practices(writer)?;
548
549    // Declared analysis blind spots
550    write_manifest_markdown_section(manifest, options.show_structural_blind_spots, jp, writer)?;
551
552    Ok(())
553}
554
555fn write_executive_summary<W: Write>(
556    metrics: &ProjectMetrics,
557    report: &ProjectBalanceReport,
558    japanese: bool,
559    writer: &mut W,
560) -> io::Result<()> {
561    writeln!(writer, "## Executive Summary\n")?;
562
563    // Health Grade with emoji
564    let grade_emoji = match report.health_grade {
565        HealthGrade::S => "⚠️",
566        HealthGrade::A => "🟢",
567        HealthGrade::B => "🟢",
568        HealthGrade::C => "🟡",
569        HealthGrade::D => "🟠",
570        HealthGrade::F => "🔴",
571    };
572
573    writeln!(
574        writer,
575        "**Health Grade**: {} {}\n",
576        grade_emoji, report.health_grade
577    )?;
578    if japanese {
579        writeln!(
580            writer,
581            "**このグレードの理由**: {}\n",
582            report.grade_rationale.summary
583        )?;
584    } else {
585        writeln!(
586            writer,
587            "**Why this grade**: {}\n",
588            report.grade_rationale.summary
589        )?;
590    }
591
592    writeln!(writer, "| Metric | Value |")?;
593    writeln!(writer, "|--------|-------|")?;
594    writeln!(writer, "| Files Analyzed | {} |", metrics.total_files)?;
595    writeln!(writer, "| Total Modules | {} |", metrics.module_count())?;
596    writeln!(writer, "| Total Couplings | {} |", report.total_couplings)?;
597    writeln!(
598        writer,
599        "| Balance Score | {:.2}/1.00 |",
600        report.average_score
601    )?;
602    writeln!(
603        writer,
604        "| Balanced | {} ({:.0}%) |",
605        report.balanced_count,
606        if report.total_couplings > 0 {
607            (report.balanced_count as f64 / report.total_couplings as f64) * 100.0
608        } else {
609            100.0
610        }
611    )?;
612    // This headline count mirrors `report.issues`/JSON `issues`; balance buckets
613    // such as Pain Zone are separate coupling classifications, not surfaced issues.
614    writeln!(writer, "| Issues Surfaced | {} |", report.issues.len())?;
615    writeln!(writer)?;
616
617    // Issue counts
618    let critical = *report
619        .issues_by_severity
620        .get(&Severity::Critical)
621        .unwrap_or(&0);
622    let high = *report.issues_by_severity.get(&Severity::High).unwrap_or(&0);
623    let medium = *report
624        .issues_by_severity
625        .get(&Severity::Medium)
626        .unwrap_or(&0);
627    let low = *report.issues_by_severity.get(&Severity::Low).unwrap_or(&0);
628
629    if critical > 0 || high > 0 {
630        writeln!(writer, "**⚠️ Action Required**\n")?;
631        if critical > 0 {
632            writeln!(
633                writer,
634                "- 🔴 **{} Critical** issues must be fixed immediately",
635                critical
636            )?;
637        }
638        if high > 0 {
639            writeln!(
640                writer,
641                "- 🟠 **{} High** priority issues should be addressed soon",
642                high
643            )?;
644        }
645        if medium > 0 {
646            writeln!(writer, "- 🟡 {} Medium priority issues to review", medium)?;
647        }
648        if low > 0 {
649            writeln!(writer, "- {} Low priority suggestions", low)?;
650        }
651        writeln!(writer)?;
652    } else if medium > 0 {
653        writeln!(
654            writer,
655            "**ℹ️ Review Suggested**: {} issues to consider.\n",
656            medium + low
657        )?;
658    } else {
659        writeln!(
660            writer,
661            "**✅ Good Health**: No significant coupling issues detected.\n"
662        )?;
663    }
664
665    Ok(())
666}
667
668fn write_refactoring_priorities<W: Write>(
669    report: &ProjectBalanceReport,
670    japanese: bool,
671    writer: &mut W,
672) -> io::Result<()> {
673    writeln!(writer, "## 🔧 Refactoring Priorities\n")?;
674
675    // Show top 5 priority issues with concrete actions
676    writeln!(writer, "### Immediate Actions\n")?;
677
678    // Deduplicate by (type, source, target): several couplings between the same
679    // module pair otherwise fill the list with identical "Immediate Actions".
680    let mut seen_priorities = std::collections::HashSet::new();
681    let priority_issues: Vec<_> = report
682        .issues
683        .iter()
684        .filter(|i| i.severity >= Severity::Medium)
685        .filter(|i| seen_priorities.insert((i.issue_type, i.source.clone(), i.target.clone())))
686        .take(5)
687        .collect();
688
689    if priority_issues.is_empty() {
690        writeln!(writer, "No immediate refactoring actions required.\n")?;
691        return Ok(());
692    }
693
694    for (i, issue) in priority_issues.iter().enumerate() {
695        let severity_icon = match issue.severity {
696            Severity::Critical => "🔴",
697            Severity::High => "🟠",
698            Severity::Medium => "🟡",
699            Severity::Low => "⚪",
700        };
701
702        writeln!(
703            writer,
704            "**{}. {} `{}` → `{}`**\n",
705            i + 1,
706            severity_icon,
707            issue.source,
708            issue.target
709        )?;
710
711        let issue_label = if japanese {
712            issue_type_japanese(issue.issue_type).to_string()
713        } else {
714            issue.issue_type.to_string()
715        };
716        let issue_description = if japanese {
717            issue_instance_description_japanese(issue)
718        } else {
719            issue.description.clone()
720        };
721        writeln!(
722            writer,
723            "- **Issue**: {} - {}",
724            issue_label, issue_description
725        )?;
726        if japanese {
727            writeln!(
728                writer,
729                "- **Why**: {}",
730                issue.issue_type.description_japanese()
731            )?;
732            writeln!(
733                writer,
734                "- **Action**: {}",
735                refactoring_action_japanese(&issue.refactoring)
736            )?;
737        } else {
738            writeln!(writer, "- **Why**: {}", issue.issue_type.description())?;
739            writeln!(writer, "- **Action**: {}", issue.refactoring)?;
740        }
741        writeln!(writer, "- **Balance Score**: {:.2}\n", issue.balance_score)?;
742    }
743
744    Ok(())
745}
746
747/// Whether an issue is driven by volatility/churn (may settle) rather than structure.
748fn issue_is_volatility_driven(issue: &CouplingIssue) -> bool {
749    match issue.issue_type {
750        IssueType::AccidentalVolatility => true,
751        IssueType::CascadingChangeRisk => issue.description.contains("accidental volatility"),
752        _ => false,
753    }
754}
755
756/// Triage surfaced issues by nature: structural (act now) vs volatility-driven
757/// (may settle). Expected-by-design patterns are downgraded elsewhere and omitted.
758fn write_issue_triage<W: Write>(
759    report: &ProjectBalanceReport,
760    japanese: bool,
761    writer: &mut W,
762) -> io::Result<()> {
763    let (volatility, structural): (Vec<_>, Vec<_>) = report
764        .issues
765        .iter()
766        .partition(|i| issue_is_volatility_driven(i));
767
768    let label = |i: &CouplingIssue| -> String {
769        let name = if japanese {
770            issue_type_japanese(i.issue_type)
771        } else {
772            // IssueType Display is English.
773            return format!("**{}** `{}` → `{}`", i.issue_type, i.source, i.target);
774        };
775        format!("**{}** `{}` → `{}`", name, i.source, i.target)
776    };
777
778    if japanese {
779        writeln!(writer, "## 課題のトリアージ\n")?;
780        writeln!(
781            writer,
782            "### 構造的な課題 — 今すぐ対応 ({} 件)\n",
783            structural.len()
784        )?;
785        for i in &structural {
786            writeln!(writer, "- {}", label(i))?;
787        }
788        writeln!(
789            writer,
790            "\n### 変更頻度由来 — 落ち着く可能性あり ({} 件)\n",
791            volatility.len()
792        )?;
793        for i in &volatility {
794            writeln!(writer, "- {}", label(i))?;
795        }
796        writeln!(
797            writer,
798            "\n> エントリポイントの広い依存や安定した中心モジュールなど、設計上想定される項目は重大度を下げて一覧から除外しています。\n"
799        )?;
800    } else {
801        writeln!(writer, "## Issue Triage\n")?;
802        writeln!(writer, "### Structural — act now ({})\n", structural.len())?;
803        for i in &structural {
804            writeln!(writer, "- {}", label(i))?;
805        }
806        writeln!(
807            writer,
808            "\n### Volatility-driven — may settle ({})\n",
809            volatility.len()
810        )?;
811        for i in &volatility {
812            writeln!(writer, "- {}", label(i))?;
813        }
814        writeln!(
815            writer,
816            "\n> Expected-by-design patterns (entrypoint fan-out, stable central abstractions) are downgraded and omitted here.\n"
817        )?;
818    }
819
820    Ok(())
821}
822
823fn write_issues_by_type<W: Write>(
824    report: &ProjectBalanceReport,
825    japanese: bool,
826    writer: &mut W,
827) -> io::Result<()> {
828    if report.issues.is_empty() {
829        return Ok(());
830    }
831
832    write_issue_triage(report, japanese, writer)?;
833
834    writeln!(writer, "## Issues by Category\n")?;
835
836    let grouped = report.issues_grouped_by_type();
837
838    // Order by severity of issues in each group
839    let mut issue_types: Vec<_> = grouped.keys().collect();
840    issue_types.sort_by(|a, b| {
841        let a_max = grouped
842            .get(a)
843            .and_then(|v| v.iter().map(|i| i.severity).max());
844        let b_max = grouped
845            .get(b)
846            .and_then(|v| v.iter().map(|i| i.severity).max());
847        b_max.cmp(&a_max)
848    });
849
850    for issue_type in issue_types {
851        if let Some(issues) = grouped.get(issue_type) {
852            let count = issues.len();
853
854            let issue_label = if japanese {
855                issue_type_japanese(*issue_type)
856            } else {
857                ""
858            };
859            if japanese {
860                writeln!(writer, "### {} ({} 件)\n", issue_label, count)?;
861                writeln!(writer, "> {}\n", issue_type.description_japanese())?;
862            } else {
863                writeln!(writer, "### {} ({} instances)\n", issue_type, count)?;
864                writeln!(writer, "> {}\n", issue_type.description())?;
865            }
866
867            // Show up to 5 examples
868            writeln!(writer, "| Severity | Source | Target | Action |")?;
869            writeln!(writer, "|----------|--------|--------|--------|")?;
870
871            for issue in issues.iter().take(5) {
872                let action_short = if japanese {
873                    refactoring_action_japanese(&issue.refactoring)
874                } else {
875                    issue.refactoring.to_string()
876                };
877                let action_truncated = truncate_chars(&action_short, 40);
878                writeln!(
879                    writer,
880                    "| {} | `{}` | `{}` | {} |",
881                    issue.severity,
882                    truncate_path(&issue.source, 25),
883                    truncate_path(&issue.target, 25),
884                    action_truncated
885                )?;
886            }
887
888            if count > 5 {
889                writeln!(writer, "\n*...and {} more instances*", count - 5)?;
890            }
891            writeln!(writer)?;
892        }
893    }
894
895    Ok(())
896}
897
898fn write_coupling_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
899    if metrics.couplings.is_empty() {
900        return Ok(());
901    }
902
903    writeln!(writer, "## Coupling Distribution\n")?;
904
905    // Strength distribution
906    writeln!(writer, "### By Integration Strength\n")?;
907    writeln!(writer, "| Strength | Count | % | Description |")?;
908    writeln!(writer, "|----------|-------|---|-------------|")?;
909
910    let total = metrics.couplings.len() as f64;
911    for (strength, label, desc) in [
912        (
913            IntegrationStrength::Contract,
914            "Contract",
915            "Depends on traits/interfaces only",
916        ),
917        (
918            IntegrationStrength::Model,
919            "Model",
920            "Uses data types/structs",
921        ),
922        (
923            IntegrationStrength::Functional,
924            "Functional",
925            "Calls specific functions",
926        ),
927        (
928            IntegrationStrength::Intrusive,
929            "Intrusive",
930            "Accesses internal details",
931        ),
932    ] {
933        let count = metrics
934            .couplings
935            .iter()
936            .filter(|c| c.strength == strength)
937            .count();
938        let pct = (count as f64 / total) * 100.0;
939        writeln!(writer, "| {} | {} | {:.0}% | {} |", label, count, pct, desc)?;
940    }
941    writeln!(writer)?;
942
943    // Distance distribution
944    writeln!(writer, "### By Distance\n")?;
945    writeln!(writer, "| Distance | Count | % |")?;
946    writeln!(writer, "|----------|-------|---|")?;
947
948    for (distance, label) in [
949        (Distance::SameModule, "Same Module (close)"),
950        (Distance::DifferentModule, "Different Module"),
951        (Distance::DifferentCrate, "External Crate (far)"),
952    ] {
953        let count = metrics
954            .couplings
955            .iter()
956            .filter(|c| c.distance == distance)
957            .count();
958        let pct = (count as f64 / total) * 100.0;
959        writeln!(writer, "| {} | {} | {:.0}% |", label, count, pct)?;
960    }
961    writeln!(writer)?;
962
963    // Volatility distribution (only for internal couplings where we have git data)
964    let internal_couplings: Vec<_> = metrics
965        .couplings
966        .iter()
967        .filter(|c| c.distance != Distance::DifferentCrate)
968        .collect();
969
970    if !internal_couplings.is_empty() {
971        let internal_total = internal_couplings.len() as f64;
972        writeln!(writer, "### By Volatility (Internal Couplings)\n")?;
973        writeln!(writer, "| Volatility | Count | % | Impact on Balance |")?;
974        writeln!(writer, "|------------|-------|---|-------------------|")?;
975
976        for (volatility, label, impact) in [
977            (
978                crate::volatility::Volatility::Low,
979                "Low (rarely changes)",
980                "No penalty",
981            ),
982            (
983                crate::volatility::Volatility::Medium,
984                "Medium (sometimes changes)",
985                "Moderate penalty",
986            ),
987            (
988                crate::volatility::Volatility::High,
989                "High (frequently changes)",
990                "Significant penalty",
991            ),
992        ] {
993            let count = internal_couplings
994                .iter()
995                .filter(|c| c.volatility == volatility)
996                .count();
997            let pct = (count as f64 / internal_total) * 100.0;
998            writeln!(
999                writer,
1000                "| {} | {} | {:.0}% | {} |",
1001                label, count, pct, impact
1002            )?;
1003        }
1004        writeln!(writer)?;
1005    }
1006
1007    // Worst balanced couplings
1008    writeln!(writer, "### Worst Balanced Couplings\n")?;
1009
1010    // External (DifferentCrate) couplings are outside our control and are excluded
1011    // from issue detection everywhere else; the crate-root re-export facade is a
1012    // stable Contract. Exclude both, and dedupe by (source, target), so this shows
1013    // distinct, actionable internal couplings.
1014    let mut seen_worst = std::collections::HashSet::new();
1015    let mut couplings_with_scores: Vec<_> = metrics
1016        .couplings
1017        .iter()
1018        .filter(|c| c.distance != Distance::DifferentCrate)
1019        .filter(|c| !is_crate_root_facade(&c.target))
1020        .filter(|c| seen_worst.insert((c.source.clone(), c.target.clone())))
1021        .map(|c| (c, BalanceScore::calculate(c)))
1022        .collect();
1023
1024    couplings_with_scores.sort_by(|a, b| {
1025        a.1.score
1026            .partial_cmp(&b.1.score)
1027            .unwrap_or(std::cmp::Ordering::Equal)
1028    });
1029
1030    writeln!(
1031        writer,
1032        "| Source | Target | Strength | Distance | Volatility | Score | Status |"
1033    )?;
1034    writeln!(
1035        writer,
1036        "|--------|--------|----------|----------|------------|-------|--------|"
1037    )?;
1038
1039    for (coupling, score) in couplings_with_scores.iter().take(15) {
1040        let strength_str = match coupling.strength {
1041            IntegrationStrength::Contract => "Contract",
1042            IntegrationStrength::Model => "Model",
1043            IntegrationStrength::Functional => "Functional",
1044            IntegrationStrength::Intrusive => "Intrusive",
1045        };
1046        let distance_str = match coupling.distance {
1047            Distance::SameFunction => "Same Fn",
1048            Distance::SameModule => "Same Mod",
1049            Distance::DifferentModule => "Diff Mod",
1050            Distance::DifferentCrate => "External",
1051        };
1052        let volatility_str = match coupling.volatility {
1053            crate::volatility::Volatility::Low => "Low",
1054            crate::volatility::Volatility::Medium => "Med",
1055            crate::volatility::Volatility::High => "High",
1056        };
1057        let status = match score.interpretation {
1058            BalanceInterpretation::Balanced => "✅ Balanced",
1059            BalanceInterpretation::Acceptable => "✅ OK",
1060            BalanceInterpretation::NeedsReview => "🟡 Review",
1061            BalanceInterpretation::NeedsRefactoring => "🟠 Refactor",
1062            BalanceInterpretation::Critical => "🔴 Critical",
1063        };
1064
1065        writeln!(
1066            writer,
1067            "| `{}` | `{}` | {} | {} | {} | {:.2} | {} |",
1068            truncate_path(&coupling.source, 20),
1069            truncate_path(&coupling.target, 20),
1070            strength_str,
1071            distance_str,
1072            volatility_str,
1073            score.score,
1074            status
1075        )?;
1076    }
1077
1078    if couplings_with_scores.len() > 15 {
1079        writeln!(
1080            writer,
1081            "\n*Showing 15 of {} couplings*",
1082            couplings_with_scores.len()
1083        )?;
1084    }
1085    writeln!(writer)?;
1086
1087    Ok(())
1088}
1089
1090fn write_module_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
1091    if metrics.modules.is_empty() {
1092        return Ok(());
1093    }
1094
1095    writeln!(writer, "## Module Statistics\n")?;
1096
1097    let show_subdomain = metrics
1098        .modules
1099        .values()
1100        .any(|module| module.subdomain.is_some());
1101    if show_subdomain {
1102        writeln!(
1103            writer,
1104            "| Module | Subdomain | Trait Impl | Inherent Impl | Internal Deps | External Deps |"
1105        )?;
1106        writeln!(
1107            writer,
1108            "|--------|-----------|------------|---------------|---------------|---------------|"
1109        )?;
1110    } else {
1111        writeln!(
1112            writer,
1113            "| Module | Trait Impl | Inherent Impl | Internal Deps | External Deps |"
1114        )?;
1115        writeln!(
1116            writer,
1117            "|--------|------------|---------------|---------------|---------------|"
1118        )?;
1119    }
1120
1121    let mut modules: Vec<_> = metrics.modules.iter().collect();
1122    modules.sort_by(|a, b| {
1123        let a_deps = a.1.internal_deps.len() + a.1.external_deps.len();
1124        let b_deps = b.1.internal_deps.len() + b.1.external_deps.len();
1125        b_deps.cmp(&a_deps)
1126    });
1127
1128    for (name, module) in modules.iter().take(20) {
1129        if show_subdomain {
1130            writeln!(
1131                writer,
1132                "| `{}` | {} | {} | {} | {} | {} |",
1133                truncate_path(name, 30),
1134                module
1135                    .subdomain
1136                    .map(|subdomain| subdomain.to_string())
1137                    .unwrap_or_else(|| "-".to_string()),
1138                module.trait_impl_count,
1139                module.inherent_impl_count,
1140                module.internal_deps.len(),
1141                module.external_deps.len()
1142            )?;
1143        } else {
1144            writeln!(
1145                writer,
1146                "| `{}` | {} | {} | {} | {} |",
1147                truncate_path(name, 30),
1148                module.trait_impl_count,
1149                module.inherent_impl_count,
1150                module.internal_deps.len(),
1151                module.external_deps.len()
1152            )?;
1153        }
1154    }
1155
1156    if modules.len() > 20 {
1157        writeln!(writer, "\n*Showing top 20 of {} modules*", modules.len())?;
1158    }
1159    writeln!(writer)?;
1160
1161    Ok(())
1162}
1163
1164fn write_volatility_section<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
1165    writeln!(writer, "## Volatility Analysis\n")?;
1166
1167    if metrics.file_changes.is_empty() {
1168        writeln!(
1169            writer,
1170            "*Git history analysis not available. Run in a git repository for volatility data.*\n"
1171        )?;
1172        return Ok(());
1173    }
1174
1175    let mut high_vol: Vec<_> = metrics
1176        .file_changes
1177        .iter()
1178        .filter(|&(_, count)| *count > 10)
1179        .collect();
1180
1181    high_vol.sort_by(|a, b| b.1.cmp(a.1));
1182
1183    if high_vol.is_empty() {
1184        writeln!(
1185            writer,
1186            "No high volatility files detected (threshold: >10 changes).\n"
1187        )?;
1188    } else {
1189        writeln!(writer, "### High Volatility Files\n")?;
1190        writeln!(
1191            writer,
1192            "⚠️ Strong coupling to these files increases cascading change risk.\n"
1193        )?;
1194        writeln!(writer, "| File | Changes |")?;
1195        writeln!(writer, "|------|---------|")?;
1196        for (file, count) in high_vol.iter().take(10) {
1197            writeln!(writer, "| `{}` | {} |", file, count)?;
1198        }
1199        writeln!(writer)?;
1200    }
1201
1202    Ok(())
1203}
1204
1205fn write_temporal_coupling_section<W: Write>(
1206    metrics: &ProjectMetrics,
1207    show_all: bool,
1208    writer: &mut W,
1209) -> io::Result<()> {
1210    if metrics.temporal_couplings.is_empty() {
1211        return Ok(());
1212    }
1213
1214    let mut strong: Vec<_> = metrics
1215        .temporal_couplings
1216        .iter()
1217        .filter(|tc| tc.is_strong())
1218        .collect();
1219    strong.sort_by(|a, b| {
1220        b.coupling_ratio
1221            .partial_cmp(&a.coupling_ratio)
1222            .unwrap_or(std::cmp::Ordering::Equal)
1223    });
1224
1225    if !show_all && strong.is_empty() {
1226        return Ok(());
1227    }
1228
1229    writeln!(writer, "## Temporal Coupling (Co-Change Analysis)\n")?;
1230    writeln!(
1231        writer,
1232        "Files that frequently change together in git commits, indicating implicit coupling"
1233    )?;
1234    writeln!(writer, "beyond what code structure reveals.\n")?;
1235
1236    if !strong.is_empty() {
1237        writeln!(
1238            writer,
1239            "### Strong Temporal Coupling (>50% co-change ratio)\n"
1240        )?;
1241        writeln!(
1242            writer,
1243            "⚠️ These pairs may share implicit knowledge (business logic, assumptions, data formats).\n"
1244        )?;
1245        writeln!(writer, "| File A | File B | Co-changes | Ratio |")?;
1246        writeln!(writer, "|--------|--------|------------|-------|")?;
1247        let strong_limit = if show_all {
1248            strong.len()
1249        } else {
1250            DEFAULT_STRONG_TEMPORAL_LIMIT
1251        };
1252        for tc in strong.iter().take(strong_limit) {
1253            writeln!(
1254                writer,
1255                "| `{}` | `{}` | {} | {:.0}% |",
1256                tc.file_a,
1257                tc.file_b,
1258                tc.co_change_count,
1259                tc.coupling_ratio * 100.0
1260            )?;
1261        }
1262        if !show_all && strong.len() > strong_limit {
1263            writeln!(
1264                writer,
1265                "\n*... and {} more (use --all)*",
1266                strong.len() - strong_limit
1267            )?;
1268        }
1269        writeln!(writer)?;
1270    }
1271
1272    if !show_all {
1273        return Ok(());
1274    }
1275
1276    let moderate: Vec<_> = metrics
1277        .temporal_couplings
1278        .iter()
1279        .filter(|tc| !tc.is_strong())
1280        .collect();
1281
1282    if !moderate.is_empty() {
1283        writeln!(writer, "### Moderate Temporal Coupling\n")?;
1284        writeln!(writer, "| File A | File B | Co-changes | Ratio |")?;
1285        writeln!(writer, "|--------|--------|------------|-------|")?;
1286        for tc in moderate {
1287            writeln!(
1288                writer,
1289                "| `{}` | `{}` | {} | {:.0}% |",
1290                tc.file_a,
1291                tc.file_b,
1292                tc.co_change_count,
1293                tc.coupling_ratio * 100.0
1294            )?;
1295        }
1296        writeln!(writer)?;
1297    }
1298
1299    Ok(())
1300}
1301
1302fn write_circular_dependencies_section<W: Write>(
1303    metrics: &ProjectMetrics,
1304    writer: &mut W,
1305) -> io::Result<()> {
1306    let summary = metrics.circular_dependency_summary();
1307
1308    if summary.total_cycles == 0 {
1309        writeln!(writer, "## Circular Dependencies\n")?;
1310        writeln!(writer, "✅ No circular dependencies detected.\n")?;
1311        return Ok(());
1312    }
1313
1314    writeln!(writer, "## ⚠️ Circular Dependencies\n")?;
1315    writeln!(
1316        writer,
1317        "Found **{} circular dependency cycle(s)** involving **{} modules**.\n",
1318        summary.total_cycles, summary.affected_modules
1319    )?;
1320
1321    writeln!(
1322        writer,
1323        "Circular dependencies make code harder to understand, test, and maintain."
1324    )?;
1325    writeln!(writer, "Consider breaking cycles by:\n")?;
1326    writeln!(writer, "1. Extracting shared types into a separate module")?;
1327    writeln!(writer, "2. Inverting dependencies using traits/interfaces")?;
1328    writeln!(writer, "3. Moving functionality to reduce coupling\n")?;
1329
1330    writeln!(writer, "### Detected Cycles\n")?;
1331
1332    for (i, cycle) in summary.cycles.iter().take(10).enumerate() {
1333        let cycle_str = cycle.join(" → ");
1334        writeln!(
1335            writer,
1336            "{}. `{}` → `{}`",
1337            i + 1,
1338            cycle_str,
1339            cycle.first().unwrap_or(&"?".to_string())
1340        )?;
1341    }
1342
1343    if summary.cycles.len() > 10 {
1344        writeln!(
1345            writer,
1346            "\n*...and {} more cycles*",
1347            summary.cycles.len() - 10
1348        )?;
1349    }
1350    writeln!(writer)?;
1351
1352    Ok(())
1353}
1354
1355fn write_best_practices<W: Write>(writer: &mut W) -> io::Result<()> {
1356    writeln!(writer, "## Balance Guidelines\n")?;
1357
1358    writeln!(
1359        writer,
1360        "The goal is **balanced coupling**, not zero coupling.\n"
1361    )?;
1362
1363    writeln!(writer, "### Ideal Patterns ✅\n")?;
1364    writeln!(writer, "| Pattern | Example | Why It Works |")?;
1365    writeln!(writer, "|---------|---------|--------------|")?;
1366    writeln!(
1367        writer,
1368        "| Strong + Close | `impl` blocks in same module | Cohesion within boundaries |"
1369    )?;
1370    writeln!(
1371        writer,
1372        "| Weak + Far | Trait impl for external crate | Loose coupling across boundaries |"
1373    )?;
1374    writeln!(writer)?;
1375
1376    writeln!(writer, "### Problematic Patterns ❌\n")?;
1377    writeln!(writer, "| Pattern | Problem | Solution |")?;
1378    writeln!(writer, "|---------|---------|----------|")?;
1379    writeln!(
1380        writer,
1381        "| Strong + Far | Global complexity | Introduce adapter or move closer |"
1382    )?;
1383    writeln!(
1384        writer,
1385        "| Strong + Volatile | Cascading changes | Add stable interface |"
1386    )?;
1387    writeln!(
1388        writer,
1389        "| Intrusive + Cross-boundary | Encapsulation violation | Extract trait API |"
1390    )?;
1391    writeln!(writer)?;
1392
1393    Ok(())
1394}
1395
1396fn truncate_path(path: &str, max_len: usize) -> String {
1397    if path.len() <= max_len {
1398        path.to_string()
1399    } else {
1400        format!("...{}", &path[path.len() - max_len + 3..])
1401    }
1402}
1403
1404fn truncate_chars(text: &str, max_chars: usize) -> String {
1405    if text.chars().count() <= max_chars {
1406        text.to_string()
1407    } else {
1408        let prefix: String = text.chars().take(max_chars).collect();
1409        format!("{}...", prefix)
1410    }
1411}
1412
1413// ===== AI-Oriented Report =====
1414
1415/// Generate AI-friendly output format for coding agents
1416///
1417/// This format is designed to be:
1418/// 1. Concise and structured for LLM consumption
1419/// 2. Actionable with specific file/module references
1420/// 3. Copy-paste ready for AI refactoring prompts
1421pub fn generate_ai_output<W: Write>(metrics: &ProjectMetrics, writer: &mut W) -> io::Result<()> {
1422    let manifest = default_manifest();
1423    generate_ai_output_with_thresholds(metrics, &IssueThresholds::default(), &manifest, writer)
1424}
1425
1426/// Generate AI-friendly output with custom thresholds
1427pub fn generate_ai_output_with_thresholds<W: Write>(
1428    metrics: &ProjectMetrics,
1429    thresholds: &IssueThresholds,
1430    manifest: &AnalysisManifest,
1431    writer: &mut W,
1432) -> io::Result<()> {
1433    let report = analyze_project_balance_with_thresholds(metrics, thresholds);
1434
1435    let project_name = metrics.workspace_name.as_deref().unwrap_or("project");
1436    writeln!(writer, "Coupling Issues in {}:", project_name)?;
1437    writeln!(
1438        writer,
1439        "────────────────────────────────────────────────────────────"
1440    )?;
1441    writeln!(writer)?;
1442
1443    // Summary line
1444    writeln!(
1445        writer,
1446        "Grade: {} | Score: {:.2} | Issues: {} High, {} Medium",
1447        report.health_grade,
1448        report.average_score,
1449        report.issues_by_severity.get(&Severity::High).unwrap_or(&0),
1450        report
1451            .issues_by_severity
1452            .get(&Severity::Medium)
1453            .unwrap_or(&0)
1454    )?;
1455    writeln!(writer, "Why this grade: {}", report.grade_rationale.summary)?;
1456    writeln!(writer)?;
1457
1458    // List issues in a structured format
1459    if report.issues.is_empty() {
1460        writeln!(writer, "✅ No coupling issues detected.")?;
1461        writeln!(writer)?;
1462    } else {
1463        writeln!(writer, "Issues:")?;
1464        writeln!(writer)?;
1465
1466        for (i, issue) in report.issues.iter().take(10).enumerate() {
1467            let severity_marker = match issue.severity {
1468                Severity::Critical => "🔴",
1469                Severity::High => "🟠",
1470                Severity::Medium => "🟡",
1471                Severity::Low => "⚪",
1472            };
1473
1474            writeln!(
1475                writer,
1476                "{}. {} {} → {}",
1477                i + 1,
1478                severity_marker,
1479                issue.source,
1480                issue.target
1481            )?;
1482            writeln!(writer, "   Type: {}", issue.issue_type)?;
1483            writeln!(writer, "   Problem: {}", issue.description)?;
1484            writeln!(writer, "   Fix: {}", issue.refactoring)?;
1485            writeln!(writer)?;
1486        }
1487
1488        if report.issues.len() > 10 {
1489            writeln!(writer, "... and {} more issues", report.issues.len() - 10)?;
1490            writeln!(writer)?;
1491        }
1492    }
1493
1494    // Circular dependencies (critical for AI to understand)
1495    let circular = metrics.circular_dependency_summary();
1496    if circular.total_cycles > 0 {
1497        writeln!(
1498            writer,
1499            "Circular Dependencies ({} cycles):",
1500            circular.total_cycles
1501        )?;
1502        for cycle in circular.cycles.iter().take(5) {
1503            writeln!(
1504                writer,
1505                "  {} → {}",
1506                cycle.join(" → "),
1507                cycle.first().unwrap_or(&"?".to_string())
1508            )?;
1509        }
1510        writeln!(writer)?;
1511    }
1512
1513    // Temporal coupling (important for AI to understand implicit dependencies)
1514    let strong_temporal: Vec<_> = metrics
1515        .temporal_couplings
1516        .iter()
1517        .filter(|tc| tc.is_strong())
1518        .collect();
1519    if !strong_temporal.is_empty() {
1520        writeln!(writer, "Temporal Coupling (implicit dependencies):")?;
1521        for tc in strong_temporal.iter().take(5) {
1522            writeln!(
1523                writer,
1524                "  {} ↔ {} ({} co-changes, {:.0}% ratio)",
1525                tc.file_a,
1526                tc.file_b,
1527                tc.co_change_count,
1528                tc.coupling_ratio * 100.0
1529            )?;
1530        }
1531        writeln!(writer)?;
1532    }
1533
1534    // only show ai refactor advice if there is something to refactor
1535    if !report.issues.is_empty() || circular.total_cycles > 0 {
1536        writeln!(
1537            writer,
1538            "────────────────────────────────────────────────────────────"
1539        )?;
1540        writeln!(writer)?;
1541
1542        // AI prompt suggestion
1543        writeln!(
1544            writer,
1545            "💡 To refactor with AI, copy this output and use this prompt:"
1546        )?;
1547        writeln!(writer)?;
1548        writeln!(writer, "```")?;
1549        writeln!(
1550            writer,
1551            "Analyze the coupling issues above from `cargo coupling --ai`. "
1552        )?;
1553        writeln!(
1554            writer,
1555            "For each issue, suggest specific code changes to reduce coupling."
1556        )?;
1557        writeln!(
1558            writer,
1559            "Focus on introducing traits, moving code closer, or breaking circular dependencies."
1560        )?;
1561        writeln!(writer, "```")?;
1562    }
1563
1564    write_manifest_summary_section(manifest, false, true, writer)?;
1565
1566    Ok(())
1567}
1568
1569fn default_manifest() -> AnalysisManifest {
1570    build_manifest(&ManifestContext {
1571        git_used: true,
1572        tests_excluded: false,
1573        parse_failures: 0,
1574        skipped_crates: Vec::new(),
1575        boundary_skipped_files: 0,
1576        dead_config_patterns: Vec::new(),
1577    })
1578}
1579
1580fn write_manifest_markdown_section<W: Write>(
1581    manifest: &AnalysisManifest,
1582    show_structural_blind_spots: bool,
1583    japanese: bool,
1584    writer: &mut W,
1585) -> io::Result<()> {
1586    if japanese {
1587        writeln!(writer, "## 未分析範囲\n")?;
1588    } else {
1589        writeln!(writer, "## Not Analyzed (blind spots)\n")?;
1590    }
1591
1592    if show_structural_blind_spots {
1593        for blind_spot in &manifest.blind_spots {
1594            let description = if japanese {
1595                blind_spot.description_ja
1596            } else {
1597                blind_spot.description
1598            };
1599            writeln!(writer, "- **{}**: {}", blind_spot.area, description)?;
1600        }
1601    }
1602
1603    let notes = manifest.localized_notes(japanese);
1604    if !notes.is_empty() {
1605        if show_structural_blind_spots {
1606            writeln!(writer)?;
1607        }
1608        if japanese {
1609            writeln!(writer, "実行時の注意:")?;
1610        } else {
1611            writeln!(writer, "Run-specific notes:")?;
1612        }
1613        for note in notes {
1614            writeln!(writer, "- {}", note)?;
1615        }
1616    }
1617
1618    if !show_structural_blind_spots {
1619        if !notes.is_empty() {
1620            writeln!(writer)?;
1621        }
1622        if japanese {
1623            writeln!(
1624                writer,
1625                "ℹ {} 件の構造的な未分析範囲があります。詳細は --blind-spots (または --json) で確認できます。",
1626                manifest.blind_spots.len()
1627            )?;
1628        } else {
1629            writeln!(
1630                writer,
1631                "ℹ {} structural blind spots not analyzed — see --blind-spots (or --json).",
1632                manifest.blind_spots.len()
1633            )?;
1634        }
1635    }
1636
1637    writeln!(writer)?;
1638    Ok(())
1639}
1640
1641fn write_manifest_summary_section<W: Write>(
1642    manifest: &AnalysisManifest,
1643    japanese: bool,
1644    show_structural_blind_spots: bool,
1645    writer: &mut W,
1646) -> io::Result<()> {
1647    if japanese {
1648        writeln!(writer, "未分析範囲:")?;
1649    } else {
1650        writeln!(writer, "Not Analyzed (blind spots):")?;
1651    }
1652
1653    if show_structural_blind_spots {
1654        for blind_spot in &manifest.blind_spots {
1655            let description = if japanese {
1656                blind_spot.description_ja
1657            } else {
1658                blind_spot.description
1659            };
1660            writeln!(writer, "  - {}: {}", blind_spot.area, description)?;
1661        }
1662    }
1663
1664    let notes = manifest.localized_notes(japanese);
1665    if !notes.is_empty() {
1666        if japanese {
1667            writeln!(writer, "実行時の注意:")?;
1668        } else {
1669            writeln!(writer, "Run-specific notes:")?;
1670        }
1671        for note in notes {
1672            writeln!(writer, "  - {}", note)?;
1673        }
1674    }
1675
1676    if !show_structural_blind_spots {
1677        if japanese {
1678            writeln!(
1679                writer,
1680                "ℹ {} 件の構造的な未分析範囲があります。詳細は --blind-spots (または --json) で確認できます。",
1681                manifest.blind_spots.len()
1682            )?;
1683        } else {
1684            writeln!(
1685                writer,
1686                "ℹ {} structural blind spots not analyzed — see --blind-spots (or --json).",
1687                manifest.blind_spots.len()
1688            )?;
1689        }
1690    }
1691
1692    writeln!(writer)?;
1693    Ok(())
1694}
1695
1696#[cfg(test)]
1697mod tests {
1698    use super::*;
1699    use crate::manifest::{ManifestContext, build_manifest};
1700    use std::path::PathBuf;
1701
1702    #[test]
1703    fn test_generate_summary() {
1704        let metrics = ProjectMetrics::new();
1705        let mut output = Vec::new();
1706
1707        let result = generate_summary(&metrics, &mut output);
1708        assert!(result.is_ok());
1709
1710        let output_str = String::from_utf8(output).unwrap();
1711        assert!(output_str.contains("Balanced Coupling Analysis"));
1712        assert!(output_str.contains("Grade:"));
1713        assert!(output_str.contains("Why this grade:"));
1714    }
1715
1716    #[test]
1717    fn test_generate_report() {
1718        let metrics = ProjectMetrics::new();
1719        let mut output = Vec::new();
1720
1721        let result = generate_report(&metrics, &mut output);
1722        assert!(result.is_ok());
1723
1724        let output_str = String::from_utf8(output).unwrap();
1725        assert!(output_str.contains("# Coupling Analysis Report"));
1726        assert!(output_str.contains("Executive Summary"));
1727        assert!(output_str.contains("**Why this grade**:"));
1728        assert!(output_str.contains("## Not Analyzed (blind spots)"));
1729        assert!(output_str.contains("4 structural blind spots not analyzed"));
1730        assert!(!output_str.contains("Dynamic connascence (Execution"));
1731    }
1732
1733    #[test]
1734    fn test_generate_summary_includes_manifest_notes_and_pointer() {
1735        let metrics = ProjectMetrics::new();
1736        let thresholds = IssueThresholds::default();
1737        let manifest = build_manifest(&ManifestContext {
1738            git_used: false,
1739            tests_excluded: true,
1740            parse_failures: 0,
1741            skipped_crates: Vec::new(),
1742            boundary_skipped_files: 0,
1743            dead_config_patterns: Vec::new(),
1744        });
1745        let mut output = Vec::new();
1746
1747        let result =
1748            generate_summary_with_thresholds(&metrics, &thresholds, &manifest, &mut output);
1749        assert!(result.is_ok());
1750
1751        let output_str = String::from_utf8(output).unwrap();
1752        assert!(output_str.contains("Not Analyzed (blind spots):"));
1753        assert!(output_str.contains("4 structural blind spots not analyzed"));
1754        assert!(!output_str.contains("Dynamic connascence (Execution"));
1755        assert!(output_str.contains("Git history was not analyzed"));
1756        assert!(output_str.contains("Test code was excluded"));
1757    }
1758
1759    #[test]
1760    fn test_text_report_blind_spots_are_opt_in() {
1761        let metrics = ProjectMetrics::new();
1762        let thresholds = IssueThresholds::default();
1763        let manifest = build_manifest(&ManifestContext {
1764            git_used: false,
1765            tests_excluded: true,
1766            parse_failures: 1,
1767            skipped_crates: Vec::new(),
1768            boundary_skipped_files: 0,
1769            dead_config_patterns: Vec::new(),
1770        });
1771
1772        let mut default_output = Vec::new();
1773        generate_report_with_options(
1774            &metrics,
1775            &thresholds,
1776            &manifest,
1777            TextReportOptions::default(),
1778            &mut default_output,
1779        )
1780        .unwrap();
1781        let default_text = String::from_utf8(default_output).unwrap();
1782        assert!(default_text.contains("4 structural blind spots not analyzed"));
1783        assert!(default_text.contains("Git history was not analyzed"));
1784        assert!(default_text.contains("Test code was excluded"));
1785        assert!(default_text.contains("1 source file(s) failed to parse"));
1786        assert!(!default_text.contains("Dynamic connascence (Execution"));
1787
1788        for options in [
1789            TextReportOptions {
1790                show_structural_blind_spots: true,
1791                show_all_temporal_couplings: false,
1792            },
1793            TextReportOptions {
1794                show_structural_blind_spots: true,
1795                show_all_temporal_couplings: true,
1796            },
1797        ] {
1798            let mut output = Vec::new();
1799            generate_report_with_options(&metrics, &thresholds, &manifest, options, &mut output)
1800                .unwrap();
1801            let text = String::from_utf8(output).unwrap();
1802            assert!(text.contains("dynamic-connascence"));
1803            assert!(text.contains("Dynamic connascence (Execution"));
1804        }
1805    }
1806
1807    #[test]
1808    fn test_text_report_temporal_coupling_default_truncates_strong_pairs() {
1809        use crate::volatility::TemporalCoupling;
1810
1811        let mut metrics = ProjectMetrics::new();
1812        metrics.temporal_couplings = (0..7)
1813            .map(|i| TemporalCoupling {
1814                file_a: format!("src/a{}.rs", i),
1815                file_b: format!("src/b{}.rs", i),
1816                co_change_count: i + 1,
1817                coupling_ratio: 0.95 - (i as f64 * 0.05),
1818            })
1819            .collect();
1820        metrics.temporal_couplings.push(TemporalCoupling {
1821            file_a: "src/moderate_a.rs".to_string(),
1822            file_b: "src/moderate_b.rs".to_string(),
1823            co_change_count: 2,
1824            coupling_ratio: 0.4,
1825        });
1826
1827        let manifest = default_manifest();
1828        let thresholds = IssueThresholds::default();
1829        let mut output = Vec::new();
1830        generate_report_with_options(
1831            &metrics,
1832            &thresholds,
1833            &manifest,
1834            TextReportOptions::default(),
1835            &mut output,
1836        )
1837        .unwrap();
1838        let text = String::from_utf8(output).unwrap();
1839        assert!(text.contains("src/a0.rs"));
1840        assert!(text.contains("src/a4.rs"));
1841        assert!(!text.contains("src/a5.rs"));
1842        assert!(!text.contains("src/moderate_a.rs"));
1843        assert!(text.contains("... and 2 more (use --all)"));
1844
1845        let mut all_output = Vec::new();
1846        generate_report_with_options(
1847            &metrics,
1848            &thresholds,
1849            &manifest,
1850            TextReportOptions {
1851                show_structural_blind_spots: false,
1852                show_all_temporal_couplings: true,
1853            },
1854            &mut all_output,
1855        )
1856        .unwrap();
1857        let all_text = String::from_utf8(all_output).unwrap();
1858        assert!(all_text.contains("src/a6.rs"));
1859        assert!(all_text.contains("src/moderate_a.rs"));
1860    }
1861
1862    #[test]
1863    fn test_report_issues_surfaced_count_matches_issue_list() {
1864        use crate::balance::project::analyze_project_balance_with_thresholds;
1865        use crate::metrics::coupling::CouplingMetrics;
1866        use crate::metrics::dimensions::{Distance, IntegrationStrength};
1867        use crate::volatility::Volatility;
1868
1869        let mut metrics = ProjectMetrics::new();
1870        metrics.add_coupling(CouplingMetrics::new(
1871            "source".to_string(),
1872            "target".to_string(),
1873            IntegrationStrength::Intrusive,
1874            Distance::DifferentModule,
1875            Volatility::High,
1876        ));
1877
1878        let thresholds = IssueThresholds::default();
1879        let report = analyze_project_balance_with_thresholds(&metrics, &thresholds);
1880        let mut output = Vec::new();
1881        generate_report_with_options(
1882            &metrics,
1883            &thresholds,
1884            &default_manifest(),
1885            TextReportOptions::default(),
1886            &mut output,
1887        )
1888        .unwrap();
1889        let text = String::from_utf8(output).unwrap();
1890        assert!(text.contains(&format!("| Issues Surfaced | {} |", report.issues.len())));
1891    }
1892
1893    #[test]
1894    fn test_generate_ai_output_includes_manifest() {
1895        let metrics = ProjectMetrics::new();
1896        let manifest = build_manifest(&ManifestContext {
1897            git_used: false,
1898            tests_excluded: false,
1899            parse_failures: 0,
1900            skipped_crates: Vec::new(),
1901            boundary_skipped_files: 0,
1902            dead_config_patterns: Vec::new(),
1903        });
1904        let mut output = Vec::new();
1905
1906        let result = generate_ai_output_with_thresholds(
1907            &metrics,
1908            &IssueThresholds::default(),
1909            &manifest,
1910            &mut output,
1911        );
1912        assert!(result.is_ok());
1913
1914        let output_str = String::from_utf8(output).unwrap();
1915        assert!(output_str.contains("Not Analyzed (blind spots):"));
1916        assert!(output_str.contains("macro-and-cfg"));
1917        assert!(output_str.contains("Git history was not analyzed"));
1918    }
1919
1920    #[test]
1921    fn test_generate_report_with_modules() {
1922        use crate::metrics::module::ModuleMetrics;
1923
1924        let mut metrics = ProjectMetrics::new();
1925        let mut module = ModuleMetrics::new(PathBuf::from("lib.rs"), "lib".to_string());
1926        module.trait_impl_count = 3;
1927        module.inherent_impl_count = 2;
1928        metrics.add_module(module);
1929
1930        let mut output = Vec::new();
1931        let result = generate_report(&metrics, &mut output);
1932        assert!(result.is_ok());
1933
1934        let output_str = String::from_utf8(output).unwrap();
1935        assert!(output_str.contains("Module Statistics"));
1936    }
1937
1938    #[test]
1939    fn test_generate_report_surfaces_subdomain_when_present() {
1940        use crate::config::Subdomain;
1941        use crate::metrics::module::ModuleMetrics;
1942
1943        let mut metrics = ProjectMetrics::new();
1944        let mut module = ModuleMetrics::new(PathBuf::from("src/report.rs"), "report".to_string());
1945        module.subdomain = Some(Subdomain::Supporting);
1946        metrics.add_module(module);
1947
1948        let mut output = Vec::new();
1949        generate_report(&metrics, &mut output).unwrap();
1950
1951        let output_str = String::from_utf8(output).unwrap();
1952        assert!(output_str.contains("| Module | Subdomain |"));
1953        assert!(output_str.contains("| `report` | Supporting |"));
1954    }
1955
1956    #[test]
1957    fn test_truncate_path() {
1958        assert_eq!(truncate_path("short", 10), "short");
1959        assert_eq!(
1960            truncate_path("this_is_a_very_long_path", 15),
1961            "...ry_long_path"
1962        );
1963    }
1964}