Skip to main content

infigraph_core/bench/
mod.rs

1use std::path::Path;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6use crate::graph::store::GraphStore;
7use crate::graph::GraphQuery;
8
9/// Quality metrics snapshot captured from the code graph and security scan.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct QualityMetrics {
12    pub timestamp: u64,
13    pub symbols: usize,
14    pub modules: usize,
15    pub calls_edges: usize,
16    pub inherits_edges: usize,
17    pub dead_code_count: usize,
18    pub security_critical: usize,
19    pub security_high: usize,
20    pub security_medium: usize,
21    pub security_low: usize,
22}
23
24/// Stored baseline with project path metadata.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct QualityBaseline {
27    pub metrics: QualityMetrics,
28    pub project_path: String,
29}
30
31impl QualityMetrics {
32    /// Capture current quality metrics from the graph store and security scanner.
33    pub fn capture(root: &Path, store: &GraphStore) -> Result<Self> {
34        let conn = store.connection()?;
35        let gq = GraphQuery::new(&conn);
36
37        let symbols = count_query(&gq, "MATCH (s:Symbol) RETURN count(s)");
38        let modules = count_query(&gq, "MATCH (m:Module) RETURN count(m)");
39        let calls_edges = count_query(&gq, "MATCH ()-[r:CALLS]->() RETURN count(r)");
40        let inherits_edges = count_query(&gq, "MATCH ()-[r:INHERITS]->() RETURN count(r)");
41
42        let dead_rows = gq
43            .raw_query(
44                "MATCH (s:Symbol) WHERE s.kind IN ['Function', 'Method'] \
45                 AND NOT EXISTS { MATCH ()-[:CALLS]->(s) } RETURN count(s)",
46            )
47            .unwrap_or_default();
48        let dead_code_count = dead_rows
49            .first()
50            .and_then(|r| r.first())
51            .and_then(|v| v.parse::<usize>().ok())
52            .unwrap_or(0);
53
54        let canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
55        let (critical, high, medium, low) = match crate::security::scan_project(&canonical) {
56            Ok(scan) => (
57                scan.critical_count(),
58                scan.high_count(),
59                scan.medium_count(),
60                scan.low_count(),
61            ),
62            Err(_) => (0, 0, 0, 0),
63        };
64
65        let timestamp = std::time::SystemTime::now()
66            .duration_since(std::time::UNIX_EPOCH)
67            .unwrap_or_default()
68            .as_secs();
69
70        Ok(Self {
71            timestamp,
72            symbols,
73            modules,
74            calls_edges,
75            inherits_edges,
76            dead_code_count,
77            security_critical: critical,
78            security_high: high,
79            security_medium: medium,
80            security_low: low,
81        })
82    }
83
84    pub fn format(&self) -> String {
85        let mut out = String::new();
86        out.push_str("\n  Metric              Value\n");
87        out.push_str("  ------------------  --------\n");
88        out.push_str(&format!("  symbols             {}\n", self.symbols));
89        out.push_str(&format!("  modules             {}\n", self.modules));
90        out.push_str(&format!("  calls_edges         {}\n", self.calls_edges));
91        out.push_str(&format!("  inherits_edges      {}\n", self.inherits_edges));
92        out.push_str(&format!("  dead_code           {}\n", self.dead_code_count));
93        out.push_str(&format!(
94            "  security_critical   {}\n",
95            self.security_critical
96        ));
97        out.push_str(&format!("  security_high       {}\n", self.security_high));
98        out.push_str(&format!("  security_medium     {}\n", self.security_medium));
99        out.push_str(&format!("  security_low        {}\n", self.security_low));
100        out
101    }
102}
103
104fn count_query(gq: &GraphQuery, cypher: &str) -> usize {
105    gq.raw_query(cypher)
106        .ok()
107        .and_then(|rows| rows.first().cloned())
108        .and_then(|row| row.first().cloned())
109        .and_then(|v| v.parse::<usize>().ok())
110        .unwrap_or(0)
111}
112
113pub fn load_baseline(root: &Path) -> Option<QualityBaseline> {
114    let path = root.join(".infigraph").join("quality_baseline.json");
115    let content = std::fs::read_to_string(&path).ok()?;
116    serde_json::from_str(&content).ok()
117}
118
119pub fn save_baseline(root: &Path, metrics: &QualityMetrics) -> Result<()> {
120    let baseline = QualityBaseline {
121        metrics: metrics.clone(),
122        project_path: root.to_string_lossy().to_string(),
123    };
124    let path = root.join(".infigraph").join("quality_baseline.json");
125    std::fs::create_dir_all(path.parent().unwrap())?;
126    let json = serde_json::to_string_pretty(&baseline)?;
127    std::fs::write(&path, json)?;
128    Ok(())
129}
130
131pub struct ComparisonResult {
132    pub metric: String,
133    pub baseline: String,
134    pub current: String,
135    pub change: String,
136    pub regression: bool,
137}
138
139pub fn compare(baseline: &QualityMetrics, current: &QualityMetrics) -> Vec<ComparisonResult> {
140    vec![
141        compare_metric("symbols", baseline.symbols, current.symbols, false),
142        compare_metric("modules", baseline.modules, current.modules, false),
143        compare_metric(
144            "calls_edges",
145            baseline.calls_edges,
146            current.calls_edges,
147            false,
148        ),
149        compare_metric(
150            "inherits_edges",
151            baseline.inherits_edges,
152            current.inherits_edges,
153            false,
154        ),
155        compare_metric(
156            "dead_code",
157            baseline.dead_code_count,
158            current.dead_code_count,
159            true,
160        ),
161        compare_metric(
162            "security_critical",
163            baseline.security_critical,
164            current.security_critical,
165            true,
166        ),
167        compare_metric(
168            "security_high",
169            baseline.security_high,
170            current.security_high,
171            true,
172        ),
173        compare_metric(
174            "security_medium",
175            baseline.security_medium,
176            current.security_medium,
177            true,
178        ),
179        compare_metric(
180            "security_low",
181            baseline.security_low,
182            current.security_low,
183            true,
184        ),
185    ]
186}
187
188fn compare_metric(
189    name: &str,
190    baseline: usize,
191    current: usize,
192    higher_is_worse: bool,
193) -> ComparisonResult {
194    let change = if baseline == 0 {
195        if current == 0 {
196            "same".to_string()
197        } else {
198            format!("+{current}")
199        }
200    } else {
201        let pct = ((current as f64 - baseline as f64) / baseline as f64 * 100.0) as i64;
202        if pct == 0 {
203            "same".to_string()
204        } else if pct > 0 {
205            format!("+{pct}%")
206        } else {
207            format!("{pct}%")
208        }
209    };
210
211    let regression = if higher_is_worse {
212        current > baseline && (current as f64 > baseline as f64 * 1.02)
213    } else {
214        current < baseline && ((current as f64) < baseline as f64 * 0.98)
215    };
216
217    ComparisonResult {
218        metric: name.to_string(),
219        baseline: baseline.to_string(),
220        current: current.to_string(),
221        change,
222        regression,
223    }
224}
225
226pub fn format_comparison(results: &[ComparisonResult]) -> String {
227    let mut out = String::new();
228    out.push_str("\n  Metric              Baseline   Current    Change\n");
229    out.push_str("  ------------------  --------   -------    ------\n");
230    for r in results {
231        let flag = if r.regression { " REGRESSION" } else { "" };
232        out.push_str(&format!(
233            "  {:<18}  {:>8}   {:>7}    {}{}\n",
234            r.metric, r.baseline, r.current, r.change, flag
235        ));
236    }
237    out
238}