Skip to main content

codemem_engine/enrichment/
hot_complex.rs

1//! Hot+complex correlation: cross-reference git churn with complexity.
2
3use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8
9impl CodememEngine {
10    /// Cross-reference git churn with complexity to find high-risk files.
11    ///
12    /// Files that are BOTH high-churn AND high-complexity represent the highest
13    /// maintenance risk. Requires E1 (complexity) and git enrichment to have run first.
14    pub fn enrich_hot_complex(
15        &self,
16        namespace: Option<&str>,
17    ) -> Result<EnrichResult, CodememError> {
18        let all_nodes = {
19            let graph = self.lock_graph()?;
20            graph.get_all_nodes()
21        };
22
23        // Find files with git churn data
24        let mut file_churn: HashMap<String, f64> = HashMap::new();
25        for node in &all_nodes {
26            if node.kind != NodeKind::File {
27                continue;
28            }
29            if let Some(churn) = node.payload.get("git_churn_rate").and_then(|v| v.as_f64()) {
30                if churn > 0.0 {
31                    file_churn.insert(node.label.clone(), churn);
32                }
33            }
34        }
35
36        // Find functions with high complexity and aggregate per file
37        let mut file_max_complexity: HashMap<String, (usize, String)> = HashMap::new();
38        for node in &all_nodes {
39            if !matches!(node.kind, NodeKind::Function | NodeKind::Method) {
40                continue;
41            }
42            let cyclomatic = node
43                .payload
44                .get("cyclomatic_complexity")
45                .and_then(|v| v.as_u64())
46                .unwrap_or(0) as usize;
47            if cyclomatic <= 5 {
48                continue;
49            }
50            let file_path = match node.payload.get("file_path").and_then(|v| v.as_str()) {
51                Some(fp) => fp.to_string(),
52                None => continue,
53            };
54            let entry = file_max_complexity
55                .entry(file_path)
56                .or_insert((0, String::new()));
57            if cyclomatic > entry.0 {
58                *entry = (cyclomatic, node.label.clone());
59            }
60        }
61
62        let mut insights_stored = 0;
63        let mut hot_complex_files: Vec<serde_json::Value> = Vec::new();
64
65        for (file_path, churn) in &file_churn {
66            if let Some((complexity, fn_name)) = file_max_complexity.get(file_path) {
67                // Both high churn and high complexity
68                hot_complex_files.push(json!({
69                    "file": file_path,
70                    "churn_rate": churn,
71                    "max_complexity": complexity,
72                    "complex_function": fn_name,
73                }));
74
75                let content = format!(
76                    "High-risk file: {} — churn rate {:.1} + max cyclomatic complexity {} (in {}). \
77                     Prioritize refactoring",
78                    file_path, churn, complexity, fn_name
79                );
80                if self
81                    .store_insight(
82                        &content,
83                        "risk",
84                        &["hot-complex"],
85                        0.9,
86                        namespace,
87                        &[format!("file:{file_path}")],
88                    )
89                    .is_some()
90                {
91                    insights_stored += 1;
92                }
93            }
94        }
95
96        self.save_index();
97
98        Ok(EnrichResult {
99            insights_stored,
100            details: json!({
101                "hot_complex_files": hot_complex_files.len(),
102                "files_with_churn": file_churn.len(),
103                "files_with_complexity": file_max_complexity.len(),
104                "insights_stored": insights_stored,
105            }),
106        })
107    }
108}