the_code_graph_domain/use_cases/
risk.rs1use crate::analysis::risk::{
2 aggregate_file_scores, compute_coupling_scores, compute_criticality_scores, compute_risk_stats,
3 compute_sensitivity, compute_test_gaps, score_symbols,
4};
5use crate::error::{CodeGraphError, Result};
6use crate::model::*;
7use crate::ports::GraphStore;
8
9pub struct RiskUseCase<S> {
10 store: S,
11}
12
13impl<S: GraphStore> RiskUseCase<S> {
14 pub fn new(store: S) -> Self {
15 Self { store }
16 }
17
18 pub fn analyze(&self, config: &RiskConfig) -> Result<RiskAnalysis> {
20 let symbols = self.store.all_symbols()?;
21 let edges = self.store.all_edges()?;
22
23 let criticality = compute_criticality_scores(&symbols, &edges);
24 let coupling = compute_coupling_scores(&symbols, &edges);
25 let test_gaps = compute_test_gaps(&symbols, &edges);
26 let sensitivity = compute_sensitivity(&symbols, &config.security_patterns);
27
28 let symbol_scores = score_symbols(
29 &symbols,
30 &criticality,
31 &coupling,
32 &test_gaps,
33 &sensitivity,
34 &config.weights,
35 );
36 let file_scores = aggregate_file_scores(&symbol_scores, &symbols);
37 let stats = compute_risk_stats(&symbol_scores, file_scores.len());
38
39 Ok(RiskAnalysis {
40 symbol_scores,
41 file_scores,
42 stats,
43 })
44 }
45
46 pub fn score_symbol(&self, qualified_name: &str, config: &RiskConfig) -> Result<RiskScore> {
48 let analysis = self.analyze(config)?;
49 analysis
50 .symbol_scores
51 .into_iter()
52 .find(|s| s.qualified_name == qualified_name)
53 .ok_or_else(|| {
54 CodeGraphError::Resolution(format!("symbol not found: {qualified_name}"))
55 })
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::test_support::InMemoryGraphStore;
63
64 fn build_store() -> InMemoryGraphStore {
65 let mut store = InMemoryGraphStore::new();
66 store.insert_symbol(SymbolNode {
68 name: "auth_handler".into(),
69 qualified_name: "src/auth.rs::auth_handler".into(),
70 kind: SymbolKind::Function,
71 location: Location {
72 file: "src/auth.rs".into(),
73 line_start: 1,
74 line_end: 20,
75 col_start: 0,
76 col_end: 0,
77 },
78 visibility: Visibility::Public,
79 is_exported: true,
80 is_async: false,
81 is_test: false,
82 decorators: vec![],
83 signature: None,
84 });
85 store.insert_symbol(SymbolNode {
87 name: "parse_input".into(),
88 qualified_name: "src/util.rs::parse_input".into(),
89 kind: SymbolKind::Function,
90 location: Location {
91 file: "src/util.rs".into(),
92 line_start: 1,
93 line_end: 10,
94 col_start: 0,
95 col_end: 0,
96 },
97 visibility: Visibility::Public,
98 is_exported: true,
99 is_async: false,
100 is_test: false,
101 decorators: vec![],
102 signature: None,
103 });
104 store.insert_edge(Edge {
106 kind: EdgeKind::Calls,
107 source: "src/auth.rs::auth_handler".into(),
108 target: "src/util.rs::parse_input".into(),
109 metadata: None,
110 });
111 store.insert_edge(Edge {
113 kind: EdgeKind::TestedBy,
114 source: "test::test_parse".into(),
115 target: "src/util.rs::parse_input".into(),
116 metadata: None,
117 });
118 store
119 }
120
121 #[test]
122 fn test_use_case_analyze() {
123 let store = build_store();
124 let uc = RiskUseCase::new(store);
125 let analysis = uc.analyze(&RiskConfig::default()).unwrap();
126 assert_eq!(analysis.symbol_scores.len(), 2);
127 assert_eq!(analysis.file_scores.len(), 2);
128 let auth_score = analysis
130 .symbol_scores
131 .iter()
132 .find(|s| s.qualified_name == "src/auth.rs::auth_handler")
133 .unwrap();
134 let util_score = analysis
135 .symbol_scores
136 .iter()
137 .find(|s| s.qualified_name == "src/util.rs::parse_input")
138 .unwrap();
139 assert!(auth_score.composite > util_score.composite);
140 assert!((auth_score.factors.sensitivity - 1.0).abs() < f64::EPSILON);
142 assert!((util_score.factors.test_gap).abs() < f64::EPSILON);
144 }
145
146 #[test]
147 fn test_use_case_score_symbol() {
148 let store = build_store();
149 let uc = RiskUseCase::new(store);
150 let score = uc
151 .score_symbol("src/auth.rs::auth_handler", &RiskConfig::default())
152 .unwrap();
153 assert_eq!(score.qualified_name, "src/auth.rs::auth_handler");
154 assert!(score.composite > 0.0);
155 }
156
157 #[test]
158 fn test_use_case_score_symbol_not_found() {
159 let store = build_store();
160 let uc = RiskUseCase::new(store);
161 let result = uc.score_symbol("nonexistent::symbol", &RiskConfig::default());
162 assert!(result.is_err());
163 }
164
165 #[test]
166 fn test_weight_normalization() {
167 let store = build_store();
168 let uc = RiskUseCase::new(store);
169 let config = RiskConfig {
171 weights: RiskWeights {
172 criticality: 1.0,
173 coupling: 1.0,
174 test_gap: 1.0,
175 sensitivity: 1.0,
176 },
177 ..RiskConfig::default()
178 };
179 let analysis = uc.analyze(&config).unwrap();
180 assert!(!analysis.symbol_scores.is_empty());
182 for score in &analysis.symbol_scores {
183 assert!(score.composite >= 0.0 && score.composite <= 1.0);
184 }
185 }
186}