Skip to main content

contextqa_core/
edge.rs

1//! Edge model for the context graph with Bayesian confidence scoring.
2//!
3//! Each edge carries a confidence score that represents our belief in the
4//! relationship's existence and current validity. Confidence is computed via
5//! multi-source Bayesian fusion and decays over time using temporal decay
6//! (TempValid framework, ACL 2024).
7//!
8//! Design decisions:
9//! - Noisy-OR for multi-path confidence (Google Knowledge Vault, KDD 2014)
10//! - Temporal decay rates vary by discovery source
11//! - Each edge tracks its full evidence provenance chain
12
13use crate::entity::DiscoverySource;
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16
17/// The type of relationship between two entities.
18///
19/// Inspired by Code Property Graph (Yamaguchi et al., IEEE S&P 2014) —
20/// layered overlays for code structure, control flow, data flow, and
21/// QA-specific relationships.
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum RelationType {
24    /// A imports/uses B (static dependency)
25    Imports,
26    /// A calls B (call graph edge)
27    Calls,
28    /// A is tested by B (test coverage mapping)
29    TestedBy,
30    /// A implements requirement B
31    Implements,
32    /// A depends on B (general dependency)
33    DependsOn,
34    /// A is required by B (requirement traceability)
35    RequiredBy,
36    /// A contains B (structural containment: module→function)
37    Contains,
38    /// A extends/inherits B
39    Extends,
40    /// A reads/writes same data as B (data flow)
41    DataFlow,
42    /// A was changed together with B historically (co-change)
43    CoChanged,
44}
45
46/// An edge in the context graph with confidence scoring and provenance.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct UcmEdge {
49    /// What kind of relationship this represents
50    pub relation_type: RelationType,
51
52    /// Fused confidence score in [0.0, 1.0]
53    /// Updated via Bayesian fusion from multiple evidence sources
54    pub confidence: f64,
55
56    /// Individual evidence sources that contributed to this edge
57    pub evidence: Vec<EvidenceSource>,
58
59    /// When this edge was first discovered
60    pub discovered_at: DateTime<Utc>,
61
62    /// When this edge was last verified (by test, trace, or re-analysis)
63    pub verified_at: Option<DateTime<Utc>>,
64
65    /// Decay rate (lambda) for temporal confidence decay
66    /// - Import statements: λ ≈ 0.001 (very slow decay)
67    /// - Call graph edges: λ ≈ 0.005 (slow decay)
68    /// - Heuristic inferences: λ ≈ 0.05 (moderate decay)
69    /// - API traffic patterns: λ ≈ 0.1 (fast decay)
70    pub decay_rate: f64,
71}
72
73/// A single piece of evidence supporting an edge's existence.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EvidenceSource {
76    /// What discovered this evidence
77    pub source: DiscoverySource,
78    /// Confidence from this individual source [0.0, 1.0]
79    pub confidence: f64,
80    /// When this evidence was collected
81    pub observed_at: DateTime<Utc>,
82    /// Human-readable description of the evidence
83    pub description: String,
84}
85
86impl UcmEdge {
87    /// Create a new edge with a single initial evidence source.
88    pub fn new(
89        relation_type: RelationType,
90        source: DiscoverySource,
91        confidence: f64,
92        description: impl Into<String>,
93    ) -> Self {
94        let now = Utc::now();
95        let decay_rate = Self::default_decay_rate(&relation_type, &source);
96
97        Self {
98            relation_type,
99            confidence,
100            evidence: vec![EvidenceSource {
101                source,
102                confidence,
103                observed_at: now,
104                description: description.into(),
105            }],
106            discovered_at: now,
107            verified_at: Some(now),
108            decay_rate,
109        }
110    }
111
112    /// Add new evidence and re-fuse confidence using Bayesian update.
113    ///
114    /// When sources agree, confidence compounds; when they conflict,
115    /// the system expresses uncertainty rather than picking a winner.
116    pub fn add_evidence(
117        &mut self,
118        source: DiscoverySource,
119        confidence: f64,
120        description: impl Into<String>,
121    ) {
122        self.evidence.push(EvidenceSource {
123            source,
124            confidence,
125            observed_at: Utc::now(),
126            description: description.into(),
127        });
128
129        // Re-fuse all evidence using noisy-OR model:
130        // P(edge) = 1 - Π(1 - P(source_i))
131        // This compounds agreement and expresses uncertainty on conflict.
132        self.confidence = crate::confidence::noisy_or(
133            &self
134                .evidence
135                .iter()
136                .map(|e| e.confidence)
137                .collect::<Vec<_>>(),
138        );
139
140        self.verified_at = Some(Utc::now());
141    }
142
143    /// Get the current confidence after applying temporal decay.
144    ///
145    /// confidence(t) = base_confidence × exp(-λ × days_since_verification)
146    /// Reference: TempValid framework (ACL 2024)
147    pub fn decayed_confidence(&self) -> f64 {
148        let last_verified = self.verified_at.unwrap_or(self.discovered_at);
149        let days_elapsed = (Utc::now() - last_verified).num_hours() as f64 / 24.0;
150        crate::confidence::temporal_decay(self.confidence, self.decay_rate, days_elapsed)
151    }
152
153    /// Mark this edge as recently verified (resets decay timer).
154    pub fn verify(&mut self) {
155        self.verified_at = Some(Utc::now());
156    }
157
158    /// Default decay rate based on relation type and discovery source.
159    fn default_decay_rate(relation_type: &RelationType, source: &DiscoverySource) -> f64 {
160        match (relation_type, source) {
161            // Import statements from static analysis — very stable
162            (RelationType::Imports, DiscoverySource::StaticAnalysis) => 0.001,
163            // Call graph from static analysis — stable
164            (RelationType::Calls, DiscoverySource::StaticAnalysis) => 0.005,
165            // Test coverage — moderately stable
166            (RelationType::TestedBy, _) => 0.01,
167            // Requirement links — slow decay
168            (RelationType::Implements | RelationType::RequiredBy, _) => 0.008,
169            // Co-change from git history — moderate decay
170            (RelationType::CoChanged, _) => 0.03,
171            // API traffic patterns — fast decay
172            (_, DiscoverySource::ApiTraffic) => 0.1,
173            // Heuristic / historical — moderate-fast decay
174            (_, DiscoverySource::HistoricalContext) => 0.05,
175            // Default
176            _ => 0.02,
177        }
178    }
179}
180
181/// Display tier for confidence scores.
182/// Dual representation: continuous internally, three-tier for UI.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub enum ConfidenceTier {
185    /// ≥ 0.85 — high confidence, solid display
186    High,
187    /// 0.60 - 0.84 — medium confidence, translucent display
188    Medium,
189    /// < 0.60 — low confidence, dashed display
190    Low,
191}
192
193impl ConfidenceTier {
194    pub fn from_score(confidence: f64) -> Self {
195        if confidence >= 0.85 {
196            Self::High
197        } else if confidence >= 0.60 {
198            Self::Medium
199        } else {
200            Self::Low
201        }
202    }
203
204    pub fn emoji(&self) -> &str {
205        match self {
206            Self::High => "🟢",
207            Self::Medium => "🟡",
208            Self::Low => "🔴",
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_edge_creation() {
219        let edge = UcmEdge::new(
220            RelationType::Imports,
221            DiscoverySource::StaticAnalysis,
222            0.95,
223            "import statement found in AST",
224        );
225        assert_eq!(edge.relation_type, RelationType::Imports);
226        assert!((edge.confidence - 0.95).abs() < f64::EPSILON);
227        assert_eq!(edge.evidence.len(), 1);
228    }
229
230    #[test]
231    fn test_evidence_fusion() {
232        let mut edge = UcmEdge::new(
233            RelationType::DependsOn,
234            DiscoverySource::StaticAnalysis,
235            0.80,
236            "static analysis found dependency",
237        );
238        edge.add_evidence(
239            DiscoverySource::ApiTraffic,
240            0.70,
241            "API traffic confirms runtime dependency",
242        );
243        // Noisy-OR: 1 - (1-0.8)(1-0.7) = 1 - 0.2*0.3 = 0.94
244        assert!((edge.confidence - 0.94).abs() < 0.01);
245        assert_eq!(edge.evidence.len(), 2);
246    }
247
248    #[test]
249    fn test_confidence_tier() {
250        assert_eq!(ConfidenceTier::from_score(0.90), ConfidenceTier::High);
251        assert_eq!(ConfidenceTier::from_score(0.72), ConfidenceTier::Medium);
252        assert_eq!(ConfidenceTier::from_score(0.45), ConfidenceTier::Low);
253    }
254}