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}