graph: extract current_metrics() from health_report

health_report() had a hidden write side effect — it saved a metrics
snapshot to disk while appearing to be a pure query (returns String).
Extract the pure computation into current_metrics(), make the save
explicit. daily_check() now uses current_metrics() too, eliminating
duplicated metric computation.
This commit is contained in:
ProofOfConcept 2026-03-05 10:24:12 -05:00
parent 2f455ba29d
commit 9eaf5e6690

View file

@ -504,7 +504,7 @@ fn label_propagation(
/// A snapshot of graph topology metrics, for tracking evolution over time /// A snapshot of graph topology metrics, for tracking evolution over time
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MetricsSnapshot { pub struct MetricsSnapshot {
pub timestamp: f64, pub timestamp: i64,
pub date: String, pub date: String,
pub nodes: usize, pub nodes: usize,
pub edges: usize, pub edges: usize,
@ -548,14 +548,39 @@ pub fn save_metrics_snapshot(snap: &MetricsSnapshot) {
} }
} }
/// Health report: summary of graph metrics /// Compute current graph metrics as a snapshot (no side effects).
pub fn current_metrics(graph: &Graph) -> MetricsSnapshot {
let now = crate::store::now_epoch();
let date = crate::store::format_datetime_space(now);
MetricsSnapshot {
timestamp: now,
date,
nodes: graph.nodes().len(),
edges: graph.edge_count(),
communities: graph.community_count(),
sigma: graph.small_world_sigma(),
alpha: graph.degree_power_law_exponent(),
gini: graph.degree_gini(),
avg_cc: graph.avg_clustering_coefficient(),
avg_path_length: graph.avg_path_length(),
}
}
/// Health report: summary of graph metrics.
/// Saves a metrics snapshot as a side effect (callers who want pure
/// computation should use `current_metrics` + `save_metrics_snapshot`).
pub fn health_report(graph: &Graph, store: &Store) -> String { pub fn health_report(graph: &Graph, store: &Store) -> String {
let n = graph.nodes().len(); let snap = current_metrics(graph);
let e = graph.edge_count(); save_metrics_snapshot(&snap);
let avg_cc = graph.avg_clustering_coefficient();
let avg_pl = graph.avg_path_length(); let n = snap.nodes;
let sigma = graph.small_world_sigma(); let e = snap.edges;
let communities = graph.community_count(); let avg_cc = snap.avg_cc;
let avg_pl = snap.avg_path_length;
let sigma = snap.sigma;
let alpha = snap.alpha;
let gini = snap.gini;
let communities = snap.communities;
// Community sizes // Community sizes
let mut comm_sizes: HashMap<u32, usize> = HashMap::new(); let mut comm_sizes: HashMap<u32, usize> = HashMap::new();
@ -576,10 +601,6 @@ pub fn health_report(graph: &Graph, store: &Store) -> String {
degrees.iter().sum::<usize>() as f64 / n as f64 degrees.iter().sum::<usize>() as f64 / n as f64
}; };
// Topology metrics
let alpha = graph.degree_power_law_exponent();
let gini = graph.degree_gini();
// Low-CC nodes: poorly integrated // Low-CC nodes: poorly integrated
let low_cc = graph.nodes().iter() let low_cc = graph.nodes().iter()
.filter(|k| graph.clustering_coefficient(k) < 0.1) .filter(|k| graph.clustering_coefficient(k) < 0.1)
@ -588,18 +609,6 @@ pub fn health_report(graph: &Graph, store: &Store) -> String {
// Category breakdown // Category breakdown
let cats = store.category_counts(); let cats = store.category_counts();
// Snapshot current metrics and log
let now = crate::store::now_epoch();
let date = crate::store::format_datetime_space(now);
let snap = MetricsSnapshot {
timestamp: now,
date: date.clone(),
nodes: n, edges: e, communities,
sigma, alpha, gini, avg_cc,
avg_path_length: avg_pl,
};
save_metrics_snapshot(&snap);
// Load history for deltas // Load history for deltas
let history = load_metrics_history(); let history = load_metrics_history();
let prev = if history.len() >= 2 { let prev = if history.len() >= 2 {