fallow_output/health_change_coupling.rs
1//! Change coupling section of the health report.
2
3/// Hidden change coupling: pairs of files that change together in the git
4/// history while no import or re-export chain connects them.
5///
6/// An agent that follows the import graph cannot find the second file of a
7/// hidden pair. The section is advisory: it has no health score weight and
8/// never gates.
9#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11pub struct ChangeCouplingReport {
12 /// History window that the section reads, for example `"12 months"`.
13 /// Without `--since` the window is 12 months, wider than the hotspot
14 /// window.
15 pub since: String,
16 /// Commits that changed at least one analyzed file and were not skipped.
17 pub commits_analyzed: usize,
18 /// Commits skipped because they changed more analyzed files than
19 /// `thresholds.max_files_per_commit`.
20 pub commits_skipped: usize,
21 /// Hidden pairs in scope before `--top` cuts `pairs`.
22 pub total_pairs: usize,
23 /// Thresholds that this run applied.
24 pub thresholds: ChangeCouplingThresholds,
25 /// Hidden pairs, sorted by `degree` and then `shared_revisions` (both
26 /// descending), then by path. Empty when the history cannot show
27 /// coupling; `workspace_diagnostics` then tells why.
28 pub pairs: Vec<HiddenCouplingPair>,
29}
30
31/// Thresholds that a hidden pair met.
32#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub struct ChangeCouplingThresholds {
35 /// Commits with more analyzed files than this are skipped.
36 pub max_files_per_commit: usize,
37 /// Minimum commits for each file of a pair.
38 pub min_revisions: u32,
39 /// Minimum commits that change both files of a pair.
40 pub min_shared_revisions: u32,
41 /// Minimum coupling degree, from 0 to 1.
42 pub min_degree: f64,
43 /// Degree at or above which a pair is `strong`.
44 pub strong_degree: f64,
45}
46
47/// Two files that change together without a static edge between them.
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50pub struct HiddenCouplingPair {
51 /// First file, relative to the project root. Sorts before `path_b`.
52 #[serde(serialize_with = "fallow_types::serde_path::serialize")]
53 pub path_a: std::path::PathBuf,
54 /// Second file, relative to the project root.
55 #[serde(serialize_with = "fallow_types::serde_path::serialize")]
56 pub path_b: std::path::PathBuf,
57 /// Commits that changed `path_a` in the window.
58 pub revisions_a: u32,
59 /// Commits that changed `path_b` in the window.
60 pub revisions_b: u32,
61 /// Commits that changed both files.
62 pub shared_revisions: u32,
63 /// Coupling degree from 0 to 1: `shared_revisions` divided by the mean of
64 /// `revisions_a` and `revisions_b`.
65 pub degree: f64,
66 /// True when `degree` reaches `thresholds.strong_degree`. The check uses
67 /// the rounded `degree` that the report shows, so 0.495 counts as 0.5.
68 /// A strong pair gives each file the `hidden_coupling` target factor. A
69 /// file that no other target rule claims becomes a
70 /// `resolve_hidden_coupling` refactoring target.
71 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
72 #[cfg_attr(feature = "schema", schemars(default))]
73 pub strong: bool,
74}