ucm_reason/
explanation.rs1use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ReasoningStep {
24 pub step: usize,
26 pub evidence: String,
28 pub inference: String,
30 pub confidence: f64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ExplanationChain {
37 pub summary: String,
39 pub steps: Vec<ReasoningStep>,
41 pub overall_confidence: f64,
43}
44
45impl ExplanationChain {
46 pub fn new(summary: impl Into<String>) -> Self {
47 Self {
48 summary: summary.into(),
49 steps: Vec::new(),
50 overall_confidence: 1.0,
51 }
52 }
53
54 pub fn add_step(
56 &mut self,
57 evidence: impl Into<String>,
58 inference: impl Into<String>,
59 confidence: f64,
60 ) -> &mut Self {
61 let step_num = self.steps.len() + 1;
62 self.steps.push(ReasoningStep {
63 step: step_num,
64 evidence: evidence.into(),
65 inference: inference.into(),
66 confidence,
67 });
68 self.overall_confidence = self.steps.iter().map(|s| s.confidence).product();
70 self
71 }
72
73 pub fn to_narrative(&self) -> String {
75 let mut narrative = format!("**{}**\n\n", self.summary);
76 for step in &self.steps {
77 narrative.push_str(&format!(
78 "Step {}: Based on {} → concluded {} (confidence: {:.0}%)\n",
79 step.step,
80 step.evidence,
81 step.inference,
82 step.confidence * 100.0
83 ));
84 }
85 narrative.push_str(&format!(
86 "\nOverall confidence: {:.0}%",
87 self.overall_confidence * 100.0
88 ));
89 narrative
90 }
91}
92
93pub fn explain_impact(entity_name: &str, path: &[String], confidence: f64) -> ExplanationChain {
95 let mut chain = ExplanationChain::new(format!("{entity_name} is impacted by this change"));
96
97 if path.len() <= 1 {
98 chain.add_step(
99 "Direct reference to changed entity found in code",
100 format!("{entity_name} directly references the changed code"),
101 confidence,
102 );
103 } else {
104 chain.add_step(
105 format!(
106 "Graph traversal found dependency path: {}",
107 path.join(" → ")
108 ),
109 format!(
110 "{entity_name} is transitively dependent via {} hops",
111 path.len() - 1
112 ),
113 confidence,
114 );
115
116 if confidence < 0.85 {
117 chain.add_step(
118 format!("Transitive confidence decays over {} hops", path.len() - 1),
119 "Each intermediate dependency reduces certainty",
120 confidence,
121 );
122 }
123 }
124
125 chain
126}
127
128pub fn explain_not_impacted(entity_name: &str, reason: &str, confidence: f64) -> ExplanationChain {
130 let mut chain = ExplanationChain::new(format!("{entity_name} is NOT impacted by this change"));
131
132 chain.add_step(
133 format!("Analyzed graph connectivity for {entity_name}"),
134 reason.to_string(),
135 confidence,
136 );
137
138 chain
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_explanation_chain() {
147 let mut chain = ExplanationChain::new("authMiddleware is directly impacted");
148 chain.add_step(
149 "git diff shows validateToken() signature changed",
150 "Return type changed from boolean to Result<Claims, AuthError>",
151 1.0,
152 );
153 chain.add_step(
154 "Static analysis: authMiddleware imports validateToken",
155 "authMiddleware must handle new Result type",
156 0.95,
157 );
158
159 assert_eq!(chain.steps.len(), 2);
160 assert!((chain.overall_confidence - 0.95).abs() < 0.01);
161
162 let narrative = chain.to_narrative();
163 assert!(narrative.contains("Step 1"));
164 assert!(narrative.contains("Step 2"));
165 }
166
167 #[test]
168 fn test_explain_impact() {
169 let chain = explain_impact(
170 "processPayment",
171 &[
172 "validateToken".into(),
173 "authMiddleware".into(),
174 "processPayment".into(),
175 ],
176 0.72,
177 );
178 assert!(!chain.steps.is_empty());
179 assert!(chain.summary.contains("processPayment"));
180 }
181
182 #[test]
183 fn test_explain_not_impacted() {
184 let chain = explain_not_impacted(
185 "generateReport",
186 "No graph path exists; uses separate admin auth flow",
187 0.88,
188 );
189 assert!(chain.summary.contains("NOT impacted"));
190 }
191}