Skip to main content

drft/
metrics.rs

1use crate::analyses::{
2    bridges::BridgesResult, change_propagation::ChangePropagationResult,
3    connected_components::ConnectedComponentsResult, degree::DegreeResult,
4    graph_stats::GraphStatsResult, pagerank::PageRankResult, scc::SccResult,
5    transitive_reduction::TransitiveReductionResult,
6};
7use crate::graph::Graph;
8
9/// A scalar metric extracted from analysis results.
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct Metric {
12    pub name: String,
13    pub value: f64,
14    pub kind: MetricKind,
15    pub dimension: String,
16}
17
18#[derive(Debug, Clone, serde::Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum MetricKind {
21    Ratio,
22    Count,
23    Score,
24}
25
26/// All known metric names, sorted alphabetically.
27pub fn all_metric_names() -> &'static [&'static str] {
28    &[
29        "average_path_length",
30        "bridge_count",
31        "component_count",
32        "cut_node_count",
33        "cycle_count",
34        "cyclomatic_complexity",
35        "density",
36        "diameter",
37        "directly_changed_count",
38        "island_ratio",
39        "max_pagerank",
40        "orphan_ratio",
41        "redundant_edge_ratio",
42        "stale_ratio",
43        "transitively_stale_count",
44    ]
45}
46
47/// Pre-computed analysis results needed for metric extraction.
48pub struct AnalysisInputs<'a> {
49    pub degree: &'a DegreeResult,
50    pub scc: &'a SccResult,
51    pub connected_components: &'a ConnectedComponentsResult,
52    pub graph_stats: &'a GraphStatsResult,
53    pub bridges: &'a BridgesResult,
54    pub transitive_reduction: &'a TransitiveReductionResult,
55    pub change_propagation: &'a ChangePropagationResult,
56    pub pagerank: &'a PageRankResult,
57}
58
59/// Compute all scalar health metrics from pre-computed analysis results.
60pub fn compute_metrics(inputs: &AnalysisInputs, graph: &Graph) -> Vec<Metric> {
61    let mut metrics: Vec<Metric> = Vec::new();
62
63    // Connectivity
64    let total_nodes = graph
65        .nodes
66        .values()
67        .filter(|n| graph.is_file_node(&n.path))
68        .count() as f64;
69    if total_nodes > 0.0 {
70        let orphans = inputs
71            .degree
72            .nodes
73            .iter()
74            .filter(|n| n.in_degree == 0)
75            .count() as f64;
76        metrics.push(Metric {
77            name: "orphan_ratio".into(),
78            value: orphans / total_nodes,
79            kind: MetricKind::Ratio,
80            dimension: "connectivity".into(),
81        });
82
83        let islands = inputs
84            .connected_components
85            .components
86            .iter()
87            .filter(|c| c.members.len() == 1)
88            .count() as f64;
89        metrics.push(Metric {
90            name: "island_ratio".into(),
91            value: islands / total_nodes,
92            kind: MetricKind::Ratio,
93            dimension: "connectivity".into(),
94        });
95    }
96
97    // Complexity
98    metrics.push(Metric {
99        name: "component_count".into(),
100        value: inputs.connected_components.component_count as f64,
101        kind: MetricKind::Count,
102        dimension: "complexity".into(),
103    });
104    metrics.push(Metric {
105        name: "density".into(),
106        value: inputs.graph_stats.density,
107        kind: MetricKind::Ratio,
108        dimension: "complexity".into(),
109    });
110    metrics.push(Metric {
111        name: "cyclomatic_complexity".into(),
112        value: (graph.edges.len() as f64 - graph.nodes.len() as f64
113            + inputs.connected_components.components.len() as f64),
114        kind: MetricKind::Count,
115        dimension: "complexity".into(),
116    });
117    if let Some(d) = inputs.graph_stats.diameter {
118        metrics.push(Metric {
119            name: "diameter".into(),
120            value: d as f64,
121            kind: MetricKind::Count,
122            dimension: "complexity".into(),
123        });
124    }
125    if let Some(avg) = inputs.graph_stats.average_path_length {
126        metrics.push(Metric {
127            name: "average_path_length".into(),
128            value: avg,
129            kind: MetricKind::Score,
130            dimension: "complexity".into(),
131        });
132    }
133
134    // Conciseness
135    let total_edges = graph.edges.len() as f64;
136    if total_edges > 0.0 {
137        metrics.push(Metric {
138            name: "redundant_edge_ratio".into(),
139            value: inputs.transitive_reduction.redundant_edges.len() as f64 / total_edges,
140            kind: MetricKind::Ratio,
141            dimension: "conciseness".into(),
142        });
143    }
144
145    // Resilience
146    metrics.push(Metric {
147        name: "bridge_count".into(),
148        value: inputs.bridges.bridges.len() as f64,
149        kind: MetricKind::Count,
150        dimension: "resilience".into(),
151    });
152    metrics.push(Metric {
153        name: "cut_node_count".into(),
154        value: inputs.bridges.cut_vertices.len() as f64,
155        kind: MetricKind::Count,
156        dimension: "resilience".into(),
157    });
158
159    // Freshness
160    if inputs.change_propagation.has_lockfile {
161        metrics.push(Metric {
162            name: "directly_changed_count".into(),
163            value: inputs.change_propagation.directly_changed.len() as f64,
164            kind: MetricKind::Count,
165            dimension: "freshness".into(),
166        });
167        metrics.push(Metric {
168            name: "transitively_stale_count".into(),
169            value: inputs.change_propagation.transitively_stale.len() as f64,
170            kind: MetricKind::Count,
171            dimension: "freshness".into(),
172        });
173        if total_nodes > 0.0 {
174            let stale = (inputs.change_propagation.directly_changed.len()
175                + inputs.change_propagation.transitively_stale.len())
176                as f64;
177            metrics.push(Metric {
178                name: "stale_ratio".into(),
179                value: stale / total_nodes,
180                kind: MetricKind::Ratio,
181                dimension: "freshness".into(),
182            });
183        }
184    }
185
186    // PageRank concentration
187    if !inputs.pagerank.nodes.is_empty() {
188        let max = inputs
189            .pagerank
190            .nodes
191            .iter()
192            .map(|n| n.score)
193            .fold(f64::NEG_INFINITY, f64::max);
194        metrics.push(Metric {
195            name: "max_pagerank".into(),
196            value: max,
197            kind: MetricKind::Score,
198            dimension: "complexity".into(),
199        });
200    }
201
202    // SCC
203    metrics.push(Metric {
204        name: "cycle_count".into(),
205        value: inputs.scc.nontrivial_count as f64,
206        kind: MetricKind::Count,
207        dimension: "complexity".into(),
208    });
209
210    metrics
211}