use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningStep {
pub step: usize,
pub evidence: String,
pub inference: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplanationChain {
pub summary: String,
pub steps: Vec<ReasoningStep>,
pub overall_confidence: f64,
}
impl ExplanationChain {
pub fn new(summary: impl Into<String>) -> Self {
Self {
summary: summary.into(),
steps: Vec::new(),
overall_confidence: 1.0,
}
}
pub fn add_step(
&mut self,
evidence: impl Into<String>,
inference: impl Into<String>,
confidence: f64,
) -> &mut Self {
let step_num = self.steps.len() + 1;
self.steps.push(ReasoningStep {
step: step_num,
evidence: evidence.into(),
inference: inference.into(),
confidence,
});
self.overall_confidence = self.steps.iter().map(|s| s.confidence).product();
self
}
pub fn to_narrative(&self) -> String {
let mut narrative = format!("**{}**\n\n", self.summary);
for step in &self.steps {
narrative.push_str(&format!(
"Step {}: Based on {} → concluded {} (confidence: {:.0}%)\n",
step.step,
step.evidence,
step.inference,
step.confidence * 100.0
));
}
narrative.push_str(&format!(
"\nOverall confidence: {:.0}%",
self.overall_confidence * 100.0
));
narrative
}
}
pub fn explain_impact(entity_name: &str, path: &[String], confidence: f64) -> ExplanationChain {
let mut chain = ExplanationChain::new(format!("{entity_name} is impacted by this change"));
if path.len() <= 1 {
chain.add_step(
"Direct reference to changed entity found in code",
format!("{entity_name} directly references the changed code"),
confidence,
);
} else {
chain.add_step(
format!(
"Graph traversal found dependency path: {}",
path.join(" → ")
),
format!(
"{entity_name} is transitively dependent via {} hops",
path.len() - 1
),
confidence,
);
if confidence < 0.85 {
chain.add_step(
format!("Transitive confidence decays over {} hops", path.len() - 1),
"Each intermediate dependency reduces certainty",
confidence,
);
}
}
chain
}
pub fn explain_not_impacted(entity_name: &str, reason: &str, confidence: f64) -> ExplanationChain {
let mut chain = ExplanationChain::new(format!("{entity_name} is NOT impacted by this change"));
chain.add_step(
format!("Analyzed graph connectivity for {entity_name}"),
reason.to_string(),
confidence,
);
chain
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_explanation_chain() {
let mut chain = ExplanationChain::new("authMiddleware is directly impacted");
chain.add_step(
"git diff shows validateToken() signature changed",
"Return type changed from boolean to Result<Claims, AuthError>",
1.0,
);
chain.add_step(
"Static analysis: authMiddleware imports validateToken",
"authMiddleware must handle new Result type",
0.95,
);
assert_eq!(chain.steps.len(), 2);
assert!((chain.overall_confidence - 0.95).abs() < 0.01);
let narrative = chain.to_narrative();
assert!(narrative.contains("Step 1"));
assert!(narrative.contains("Step 2"));
}
#[test]
fn test_explain_impact() {
let chain = explain_impact(
"processPayment",
&[
"validateToken".into(),
"authMiddleware".into(),
"processPayment".into(),
],
0.72,
);
assert!(!chain.steps.is_empty());
assert!(chain.summary.contains("processPayment"));
}
#[test]
fn test_explain_not_impacted() {
let chain = explain_not_impacted(
"generateReport",
"No graph path exists; uses separate admin auth flow",
0.88,
);
assert!(chain.summary.contains("NOT impacted"));
}
}