use crate::analysis::risk::{
aggregate_file_scores, compute_coupling_scores, compute_criticality_scores, compute_risk_stats,
compute_sensitivity, compute_test_gaps, score_symbols,
};
use crate::error::{CodeGraphError, Result};
use crate::model::*;
use crate::ports::GraphStore;
pub struct RiskUseCase<S> {
store: S,
}
impl<S: GraphStore> RiskUseCase<S> {
pub fn new(store: S) -> Self {
Self { store }
}
pub fn analyze(&self, config: &RiskConfig) -> Result<RiskAnalysis> {
let symbols = self.store.all_symbols()?;
let edges = self.store.all_edges()?;
let criticality = compute_criticality_scores(&symbols, &edges);
let coupling = compute_coupling_scores(&symbols, &edges);
let test_gaps = compute_test_gaps(&symbols, &edges);
let sensitivity = compute_sensitivity(&symbols, &config.security_patterns);
let symbol_scores = score_symbols(
&symbols,
&criticality,
&coupling,
&test_gaps,
&sensitivity,
&config.weights,
);
let file_scores = aggregate_file_scores(&symbol_scores, &symbols);
let stats = compute_risk_stats(&symbol_scores, file_scores.len());
Ok(RiskAnalysis {
symbol_scores,
file_scores,
stats,
})
}
pub fn score_symbol(&self, qualified_name: &str, config: &RiskConfig) -> Result<RiskScore> {
let analysis = self.analyze(config)?;
analysis
.symbol_scores
.into_iter()
.find(|s| s.qualified_name == qualified_name)
.ok_or_else(|| {
CodeGraphError::Resolution(format!("symbol not found: {qualified_name}"))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::InMemoryGraphStore;
fn build_store() -> InMemoryGraphStore {
let mut store = InMemoryGraphStore::new();
store.insert_symbol(SymbolNode {
name: "auth_handler".into(),
qualified_name: "src/auth.rs::auth_handler".into(),
kind: SymbolKind::Function,
location: Location {
file: "src/auth.rs".into(),
line_start: 1,
line_end: 20,
col_start: 0,
col_end: 0,
},
visibility: Visibility::Public,
is_exported: true,
is_async: false,
is_test: false,
decorators: vec![],
signature: None,
});
store.insert_symbol(SymbolNode {
name: "parse_input".into(),
qualified_name: "src/util.rs::parse_input".into(),
kind: SymbolKind::Function,
location: Location {
file: "src/util.rs".into(),
line_start: 1,
line_end: 10,
col_start: 0,
col_end: 0,
},
visibility: Visibility::Public,
is_exported: true,
is_async: false,
is_test: false,
decorators: vec![],
signature: None,
});
store.insert_edge(Edge {
kind: EdgeKind::Calls,
source: "src/auth.rs::auth_handler".into(),
target: "src/util.rs::parse_input".into(),
metadata: None,
});
store.insert_edge(Edge {
kind: EdgeKind::TestedBy,
source: "test::test_parse".into(),
target: "src/util.rs::parse_input".into(),
metadata: None,
});
store
}
#[test]
fn test_use_case_analyze() {
let store = build_store();
let uc = RiskUseCase::new(store);
let analysis = uc.analyze(&RiskConfig::default()).unwrap();
assert_eq!(analysis.symbol_scores.len(), 2);
assert_eq!(analysis.file_scores.len(), 2);
let auth_score = analysis
.symbol_scores
.iter()
.find(|s| s.qualified_name == "src/auth.rs::auth_handler")
.unwrap();
let util_score = analysis
.symbol_scores
.iter()
.find(|s| s.qualified_name == "src/util.rs::parse_input")
.unwrap();
assert!(auth_score.composite > util_score.composite);
assert!((auth_score.factors.sensitivity - 1.0).abs() < f64::EPSILON);
assert!((util_score.factors.test_gap).abs() < f64::EPSILON);
}
#[test]
fn test_use_case_score_symbol() {
let store = build_store();
let uc = RiskUseCase::new(store);
let score = uc
.score_symbol("src/auth.rs::auth_handler", &RiskConfig::default())
.unwrap();
assert_eq!(score.qualified_name, "src/auth.rs::auth_handler");
assert!(score.composite > 0.0);
}
#[test]
fn test_use_case_score_symbol_not_found() {
let store = build_store();
let uc = RiskUseCase::new(store);
let result = uc.score_symbol("nonexistent::symbol", &RiskConfig::default());
assert!(result.is_err());
}
#[test]
fn test_weight_normalization() {
let store = build_store();
let uc = RiskUseCase::new(store);
let config = RiskConfig {
weights: RiskWeights {
criticality: 1.0,
coupling: 1.0,
test_gap: 1.0,
sensitivity: 1.0,
},
..RiskConfig::default()
};
let analysis = uc.analyze(&config).unwrap();
assert!(!analysis.symbol_scores.is_empty());
for score in &analysis.symbol_scores {
assert!(score.composite >= 0.0 && score.composite <= 1.0);
}
}
}