Skip to main content

arbor_graph/
confidence.rs

1//! Confidence scoring for impact analysis
2//!
3//! Provides explainable risk levels (Low/Medium/High) based on graph structure.
4
5use crate::ImpactAnalysis;
6
7/// Confidence level for an analysis result
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ConfidenceLevel {
10    /// High confidence - well-connected node with clear edges
11    High,
12    /// Medium confidence - some uncertainty exists
13    Medium,
14    /// Low confidence - significant unknowns
15    Low,
16}
17
18impl std::fmt::Display for ConfidenceLevel {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            ConfidenceLevel::High => write!(f, "High"),
22            ConfidenceLevel::Medium => write!(f, "Medium"),
23            ConfidenceLevel::Low => write!(f, "Low"),
24        }
25    }
26}
27
28/// Reasons explaining the confidence level
29#[derive(Debug, Clone)]
30pub struct ConfidenceExplanation {
31    pub level: ConfidenceLevel,
32    pub reasons: Vec<String>,
33    pub suggestions: Vec<String>,
34}
35
36impl ConfidenceExplanation {
37    /// Compute confidence from an impact analysis
38    pub fn from_analysis(analysis: &ImpactAnalysis) -> Self {
39        let mut reasons = Vec::new();
40        let mut suggestions = Vec::new();
41
42        let upstream_count = analysis.upstream.len();
43        let downstream_count = analysis.downstream.len();
44        let total = analysis.total_affected;
45
46        // Determine base confidence from connectivity
47        let level = if upstream_count == 0 && downstream_count == 0 {
48            // Isolated node
49            reasons.push("Node appears isolated (no detected connections)".to_string());
50            suggestions
51                .push("Verify if this is called dynamically or from external code".to_string());
52            ConfidenceLevel::Low
53        } else if upstream_count == 0 {
54            // Entry point
55            reasons.push("Node is an entry point (no internal callers)".to_string());
56            reasons.push(format!("Has {} downstream dependencies", downstream_count));
57            if downstream_count > 5 {
58                suggestions.push("Consider impact on downstream dependencies".to_string());
59                ConfidenceLevel::Medium
60            } else {
61                ConfidenceLevel::High
62            }
63        } else if downstream_count == 0 {
64            // Leaf/utility node
65            reasons.push("Node is a utility (no outgoing dependencies)".to_string());
66            reasons.push(format!("Called by {} upstream nodes", upstream_count));
67            ConfidenceLevel::High
68        } else {
69            // Connected node
70            reasons.push(format!(
71                "{} callers, {} dependencies",
72                upstream_count, downstream_count
73            ));
74
75            if total > 20 {
76                reasons.push("Large blast radius detected".to_string());
77                suggestions
78                    .push("Consider breaking this change into smaller refactors".to_string());
79                ConfidenceLevel::Medium
80            } else if total > 50 {
81                reasons.push("Very large blast radius".to_string());
82                suggestions
83                    .push("This change affects a significant portion of the codebase".to_string());
84                ConfidenceLevel::Low
85            } else {
86                reasons.push("Well-connected with manageable impact".to_string());
87                ConfidenceLevel::High
88            }
89        };
90
91        // Add structural insights
92        if total > 0 {
93            let direct_count = analysis
94                .upstream
95                .iter()
96                .filter(|n| n.hop_distance == 1)
97                .count();
98            if direct_count > 0 {
99                reasons.push(format!("{} nodes will break immediately", direct_count));
100            }
101        }
102
103        // Standard disclaimer
104        suggestions.push("Tests still recommended for behavioral verification".to_string());
105
106        Self {
107            level,
108            reasons,
109            suggestions,
110        }
111    }
112}
113
114/// Node role classification
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum NodeRole {
117    /// Entry point - receives control from outside
118    EntryPoint,
119    /// Utility - helper function called by others
120    Utility,
121    /// Core logic - central to the domain
122    CoreLogic,
123    /// Isolated - no detected connections
124    Isolated,
125    /// Adapter - boundary between layers
126    Adapter,
127}
128
129impl std::fmt::Display for NodeRole {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            NodeRole::EntryPoint => write!(f, "Entry Point"),
133            NodeRole::Utility => write!(f, "Utility"),
134            NodeRole::CoreLogic => write!(f, "Core Logic"),
135            NodeRole::Isolated => write!(f, "Isolated"),
136            NodeRole::Adapter => write!(f, "Adapter"),
137        }
138    }
139}
140
141impl NodeRole {
142    /// Determine role from impact analysis
143    pub fn from_analysis(analysis: &ImpactAnalysis) -> Self {
144        let has_upstream = !analysis.upstream.is_empty();
145        let has_downstream = !analysis.downstream.is_empty();
146
147        match (has_upstream, has_downstream) {
148            (false, false) => NodeRole::Isolated,
149            (false, true) => NodeRole::EntryPoint,
150            (true, false) => NodeRole::Utility,
151            (true, true) => {
152                // Distinguish between adapter and core logic
153                let upstream_count = analysis.upstream.len();
154                let downstream_count = analysis.downstream.len();
155
156                // Adapters typically have few callers but many dependencies (or vice versa)
157                if (upstream_count <= 2 && downstream_count > 5)
158                    || (downstream_count <= 2 && upstream_count > 5)
159                {
160                    NodeRole::Adapter
161                } else {
162                    NodeRole::CoreLogic
163                }
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_confidence_level_display() {
175        assert_eq!(ConfidenceLevel::High.to_string(), "High");
176        assert_eq!(ConfidenceLevel::Medium.to_string(), "Medium");
177        assert_eq!(ConfidenceLevel::Low.to_string(), "Low");
178    }
179
180    #[test]
181    fn test_node_role_display() {
182        assert_eq!(NodeRole::EntryPoint.to_string(), "Entry Point");
183        assert_eq!(NodeRole::Utility.to_string(), "Utility");
184        assert_eq!(NodeRole::CoreLogic.to_string(), "Core Logic");
185        assert_eq!(NodeRole::Isolated.to_string(), "Isolated");
186        assert_eq!(NodeRole::Adapter.to_string(), "Adapter");
187    }
188}