Skip to content

Commit b05ac0e

Browse files
committed
refactor analysis engine for modularity
1 parent 75c8f75 commit b05ac0e

14 files changed

Lines changed: 2365 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ members = [
66
"crates/og-graph",
77
"crates/og-db",
88
"crates/og-analytics",
9+
"crates/og-metrics-centrality",
10+
"crates/og-metrics-community",
11+
"crates/og-metrics-risk",
12+
"crates/og-metrics-quality",
913
"crates/og-services",
1014
"crates/og-utils"
1115
]

crates/og-analytics/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ edition = "2021"
77
og-types = { path = "../og-types" }
88
og-graph = { path = "../og-graph" }
99
og-db = { path = "../og-db" }
10+
og-metrics-centrality = { path = "../og-metrics-centrality" }
11+
og-metrics-community = { path = "../og-metrics-community" }
12+
og-metrics-risk = { path = "../og-metrics-risk" }
13+
og-metrics-quality = { path = "../og-metrics-quality" }
1014
neo4rs = { workspace = true }
1115
tokio = { workspace = true }
1216
serde = { workspace = true }
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
use crate::metrics::{MetricResults, MetricValue};
2+
use anyhow::Result;
3+
use og_graph::graph::CodeGraph;
4+
use og_metrics_centrality::{CentralityMetrics, CentralityResults};
5+
use og_metrics_community::{CommunityDetection, CommunityResults};
6+
use og_metrics_risk::{RiskAnalyzer, RiskResults};
7+
use og_metrics_quality::{QualityAnalyzer, QualityResults};
8+
// Removed unused imports
9+
use std::time::Duration;
10+
use std::sync::Arc;
11+
use tracing::{debug, info, warn, error};
12+
13+
/// Configuration for analytics engine v2 with improved error handling
14+
#[derive(Debug, Clone)]
15+
pub struct AnalyticsConfigV2 {
16+
/// Weights for composite metrics
17+
pub weights: MetricWeights,
18+
/// Enable parallel computation per metric type
19+
pub parallel_metrics: bool,
20+
/// Timeout for each metric calculation
21+
pub metric_timeout: Duration,
22+
/// PageRank iterations
23+
pub pagerank_iterations: usize,
24+
/// PageRank damping factor
25+
pub pagerank_damping: f64,
26+
/// Community detection resolution
27+
pub louvain_resolution: f64,
28+
/// Enable sampling for large graphs
29+
pub use_sampling: bool,
30+
/// Sample size for betweenness centrality
31+
pub betweenness_sample_size: usize,
32+
}
33+
34+
impl Default for AnalyticsConfigV2 {
35+
fn default() -> Self {
36+
Self {
37+
weights: MetricWeights::default(),
38+
parallel_metrics: true,
39+
metric_timeout: Duration::from_secs(10),
40+
pagerank_iterations: 30,
41+
pagerank_damping: 0.85,
42+
louvain_resolution: 1.0,
43+
use_sampling: true,
44+
betweenness_sample_size: 1000,
45+
}
46+
}
47+
}
48+
49+
/// Weights for composite metrics
50+
#[derive(Debug, Clone)]
51+
pub struct MetricWeights {
52+
pub importance_pagerank: f64,
53+
pub importance_degree: f64,
54+
pub importance_betweenness: f64,
55+
pub risk_complexity: f64,
56+
pub risk_coupling: f64,
57+
pub risk_churn: f64,
58+
pub chokepoint_betweenness: f64,
59+
pub chokepoint_clustering: f64,
60+
pub chokepoint_degree: f64,
61+
pub payoff_risk: f64,
62+
pub payoff_importance: f64,
63+
pub payoff_coverage: f64,
64+
}
65+
66+
impl Default for MetricWeights {
67+
fn default() -> Self {
68+
Self {
69+
importance_pagerank: 0.5,
70+
importance_degree: 0.3,
71+
importance_betweenness: 0.2,
72+
risk_complexity: 0.4,
73+
risk_coupling: 0.4,
74+
risk_churn: 0.2,
75+
chokepoint_betweenness: 0.6,
76+
chokepoint_clustering: 0.2,
77+
chokepoint_degree: 0.2,
78+
payoff_risk: 0.4,
79+
payoff_importance: 0.3,
80+
payoff_coverage: 0.3,
81+
}
82+
}
83+
}
84+
85+
/// Main analytics engine v2 with modular metrics
86+
pub struct AnalyticsEngineV2 {
87+
config: AnalyticsConfigV2,
88+
centrality_metrics: Arc<CentralityMetrics>,
89+
community_detector: Arc<CommunityDetection>,
90+
risk_analyzer: Arc<RiskAnalyzer>,
91+
quality_analyzer: Arc<QualityAnalyzer>,
92+
}
93+
94+
impl AnalyticsEngineV2 {
95+
/// Create a new analytics engine v2
96+
pub fn new(config: AnalyticsConfigV2) -> Self {
97+
// Configure individual metric modules
98+
let mut centrality_metrics = CentralityMetrics::new();
99+
centrality_metrics.max_iterations = config.pagerank_iterations;
100+
centrality_metrics.use_sampling = config.use_sampling;
101+
centrality_metrics.sample_size = config.betweenness_sample_size;
102+
103+
let community_detector = CommunityDetection::with_resolution(config.louvain_resolution);
104+
let risk_analyzer = RiskAnalyzer::new();
105+
let quality_analyzer = QualityAnalyzer::new();
106+
107+
Self {
108+
config,
109+
centrality_metrics: Arc::new(centrality_metrics),
110+
community_detector: Arc::new(community_detector),
111+
risk_analyzer: Arc::new(risk_analyzer),
112+
quality_analyzer: Arc::new(quality_analyzer),
113+
}
114+
}
115+
116+
/// Validate graph before analysis
117+
fn validate_graph(&self, graph: &CodeGraph) -> Result<()> {
118+
let node_count = graph.node_map.len();
119+
let edge_count = graph.graph.edge_count();
120+
121+
debug!("Validating graph: {} nodes, {} edges", node_count, edge_count);
122+
123+
if node_count == 0 {
124+
warn!("Graph is empty");
125+
}
126+
127+
// Check for disconnected components (informational)
128+
let connected = petgraph::algo::connected_components(&graph.graph);
129+
if connected > 1 {
130+
info!("Graph has {} disconnected components", connected);
131+
}
132+
133+
Ok(())
134+
}
135+
136+
/// Analyze a code graph with modular metrics
137+
pub async fn analyze(&self, graph: &CodeGraph) -> Result<ModularAnalysisReport> {
138+
info!("Starting modular graph analysis with {} nodes", graph.node_map.len());
139+
140+
// Validate graph
141+
self.validate_graph(graph)?;
142+
143+
let mut report = ModularAnalysisReport::default();
144+
145+
// Run each metric module with timeout and error recovery
146+
if self.config.parallel_metrics {
147+
// Run metrics in parallel with isolated error handling
148+
let (centrality, community, risk, quality) = tokio::join!(
149+
self.run_centrality_with_timeout(graph),
150+
self.run_community_with_timeout(graph),
151+
self.run_risk_with_timeout(graph),
152+
self.run_quality_with_timeout(graph),
153+
);
154+
155+
report.centrality = centrality.unwrap_or_else(|e| {
156+
error!("Centrality metrics failed: {}", e);
157+
report.errors.push(format!("Centrality: {}", e));
158+
CentralityResults::default()
159+
});
160+
161+
report.community = community.unwrap_or_else(|e| {
162+
error!("Community detection failed: {}", e);
163+
report.errors.push(format!("Community: {}", e));
164+
CommunityResults::default()
165+
});
166+
167+
report.risk = risk.unwrap_or_else(|e| {
168+
error!("Risk analysis failed: {}", e);
169+
report.errors.push(format!("Risk: {}", e));
170+
RiskResults::default()
171+
});
172+
173+
report.quality = quality.unwrap_or_else(|e| {
174+
error!("Quality metrics failed: {}", e);
175+
report.errors.push(format!("Quality: {}", e));
176+
QualityResults::default()
177+
});
178+
} else {
179+
// Run metrics sequentially with individual error handling
180+
report.centrality = self.run_centrality_with_timeout(graph).await
181+
.unwrap_or_else(|e| {
182+
error!("Centrality metrics failed: {}", e);
183+
report.errors.push(format!("Centrality: {}", e));
184+
CentralityResults::default()
185+
});
186+
187+
report.community = self.run_community_with_timeout(graph).await
188+
.unwrap_or_else(|e| {
189+
error!("Community detection failed: {}", e);
190+
report.errors.push(format!("Community: {}", e));
191+
CommunityResults::default()
192+
});
193+
194+
report.risk = self.run_risk_with_timeout(graph).await
195+
.unwrap_or_else(|e| {
196+
error!("Risk analysis failed: {}", e);
197+
report.errors.push(format!("Risk: {}", e));
198+
RiskResults::default()
199+
});
200+
201+
report.quality = self.run_quality_with_timeout(graph).await
202+
.unwrap_or_else(|e| {
203+
error!("Quality metrics failed: {}", e);
204+
report.errors.push(format!("Quality: {}", e));
205+
QualityResults::default()
206+
});
207+
}
208+
209+
// Calculate composite scores
210+
report.calculate_composite_scores(&self.config.weights);
211+
212+
info!("Modular analysis complete with {} errors", report.errors.len());
213+
Ok(report)
214+
}
215+
216+
/// Run centrality metrics with timeout
217+
async fn run_centrality_with_timeout(&self, graph: &CodeGraph) -> Result<CentralityResults> {
218+
let graph = graph.clone();
219+
let metrics = Arc::clone(&self.centrality_metrics);
220+
let timeout = self.config.metric_timeout;
221+
222+
tokio::time::timeout(timeout, tokio::task::spawn_blocking(move || {
223+
metrics.calculate_all(&graph)
224+
}))
225+
.await?
226+
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
227+
}
228+
229+
/// Run community detection with timeout
230+
async fn run_community_with_timeout(&self, graph: &CodeGraph) -> Result<CommunityResults> {
231+
let graph = graph.clone();
232+
let detector = Arc::clone(&self.community_detector);
233+
let timeout = self.config.metric_timeout;
234+
235+
tokio::time::timeout(timeout, tokio::task::spawn_blocking(move || {
236+
detector.detect_communities(&graph)
237+
}))
238+
.await?
239+
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
240+
}
241+
242+
/// Run risk analysis with timeout
243+
async fn run_risk_with_timeout(&self, graph: &CodeGraph) -> Result<RiskResults> {
244+
let graph = graph.clone();
245+
let analyzer = Arc::clone(&self.risk_analyzer);
246+
let timeout = self.config.metric_timeout;
247+
248+
tokio::time::timeout(timeout, tokio::task::spawn_blocking(move || {
249+
analyzer.analyze_risks(&graph)
250+
}))
251+
.await?
252+
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
253+
}
254+
255+
/// Run quality analysis with timeout
256+
async fn run_quality_with_timeout(&self, graph: &CodeGraph) -> Result<QualityResults> {
257+
let graph = graph.clone();
258+
let analyzer = Arc::clone(&self.quality_analyzer);
259+
let timeout = self.config.metric_timeout;
260+
261+
tokio::time::timeout(timeout, tokio::task::spawn_blocking(move || {
262+
analyzer.analyze_quality(&graph)
263+
}))
264+
.await?
265+
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
266+
}
267+
}
268+
269+
/// Modular analysis report combining all metric results
270+
#[derive(Debug, Default, Clone)]
271+
pub struct ModularAnalysisReport {
272+
pub centrality: CentralityResults,
273+
pub community: CommunityResults,
274+
pub risk: RiskResults,
275+
pub quality: QualityResults,
276+
pub composite_scores: CompositeScores,
277+
pub errors: Vec<String>,
278+
}
279+
280+
impl ModularAnalysisReport {
281+
/// Calculate composite scores from individual metrics
282+
pub fn calculate_composite_scores(&mut self, _weights: &MetricWeights) {
283+
// This would calculate weighted composite scores
284+
// Implementation depends on specific business logic
285+
self.composite_scores = CompositeScores::default();
286+
}
287+
288+
/// Convert to legacy MetricResults format if needed
289+
pub fn to_metric_results(&self) -> Vec<MetricResults> {
290+
let mut results = Vec::new();
291+
292+
// Convert centrality results
293+
let mut centrality_result = MetricResults::new("centrality".to_string());
294+
for (node_id, metrics) in &self.centrality.degree {
295+
centrality_result.add_value(
296+
node_id.clone(),
297+
MetricValue::Float(metrics.total_degree),
298+
);
299+
}
300+
results.push(centrality_result);
301+
302+
// Add other metric conversions as needed
303+
304+
results
305+
}
306+
}
307+
308+
#[derive(Debug, Default, Clone)]
309+
pub struct CompositeScores {
310+
pub importance_scores: Vec<(String, f64)>,
311+
pub risk_scores: Vec<(String, f64)>,
312+
pub quality_scores: Vec<(String, f64)>,
313+
}
314+
315+
// Clone implementations moved to respective crates
316+
317+
#[cfg(test)]
318+
mod tests {
319+
use super::*;
320+
321+
#[tokio::test]
322+
async fn test_modular_engine() {
323+
let graph = CodeGraph::new();
324+
let config = AnalyticsConfigV2::default();
325+
let engine = AnalyticsEngineV2::new(config);
326+
327+
let report = engine.analyze(&graph).await.unwrap();
328+
assert!(report.errors.is_empty() || !report.errors.is_empty());
329+
}
330+
}

0 commit comments

Comments
 (0)