use crate::entity::DiscoverySource;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationType {
Imports,
Calls,
TestedBy,
Implements,
DependsOn,
RequiredBy,
Contains,
Extends,
DataFlow,
CoChanged,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UcmEdge {
pub relation_type: RelationType,
pub confidence: f64,
pub evidence: Vec<EvidenceSource>,
pub discovered_at: DateTime<Utc>,
pub verified_at: Option<DateTime<Utc>>,
pub decay_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceSource {
pub source: DiscoverySource,
pub confidence: f64,
pub observed_at: DateTime<Utc>,
pub description: String,
}
impl UcmEdge {
pub fn new(
relation_type: RelationType,
source: DiscoverySource,
confidence: f64,
description: impl Into<String>,
) -> Self {
let now = Utc::now();
let decay_rate = Self::default_decay_rate(&relation_type, &source);
Self {
relation_type,
confidence,
evidence: vec![EvidenceSource {
source,
confidence,
observed_at: now,
description: description.into(),
}],
discovered_at: now,
verified_at: Some(now),
decay_rate,
}
}
pub fn add_evidence(
&mut self,
source: DiscoverySource,
confidence: f64,
description: impl Into<String>,
) {
self.evidence.push(EvidenceSource {
source,
confidence,
observed_at: Utc::now(),
description: description.into(),
});
self.confidence = crate::confidence::noisy_or(
&self
.evidence
.iter()
.map(|e| e.confidence)
.collect::<Vec<_>>(),
);
self.verified_at = Some(Utc::now());
}
pub fn decayed_confidence(&self) -> f64 {
let last_verified = self.verified_at.unwrap_or(self.discovered_at);
let days_elapsed = (Utc::now() - last_verified).num_hours() as f64 / 24.0;
crate::confidence::temporal_decay(self.confidence, self.decay_rate, days_elapsed)
}
pub fn verify(&mut self) {
self.verified_at = Some(Utc::now());
}
fn default_decay_rate(relation_type: &RelationType, source: &DiscoverySource) -> f64 {
match (relation_type, source) {
(RelationType::Imports, DiscoverySource::StaticAnalysis) => 0.001,
(RelationType::Calls, DiscoverySource::StaticAnalysis) => 0.005,
(RelationType::TestedBy, _) => 0.01,
(RelationType::Implements | RelationType::RequiredBy, _) => 0.008,
(RelationType::CoChanged, _) => 0.03,
(_, DiscoverySource::ApiTraffic) => 0.1,
(_, DiscoverySource::HistoricalContext) => 0.05,
_ => 0.02,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConfidenceTier {
High,
Medium,
Low,
}
impl ConfidenceTier {
pub fn from_score(confidence: f64) -> Self {
if confidence >= 0.85 {
Self::High
} else if confidence >= 0.60 {
Self::Medium
} else {
Self::Low
}
}
pub fn emoji(&self) -> &str {
match self {
Self::High => "🟢",
Self::Medium => "🟡",
Self::Low => "🔴",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edge_creation() {
let edge = UcmEdge::new(
RelationType::Imports,
DiscoverySource::StaticAnalysis,
0.95,
"import statement found in AST",
);
assert_eq!(edge.relation_type, RelationType::Imports);
assert!((edge.confidence - 0.95).abs() < f64::EPSILON);
assert_eq!(edge.evidence.len(), 1);
}
#[test]
fn test_evidence_fusion() {
let mut edge = UcmEdge::new(
RelationType::DependsOn,
DiscoverySource::StaticAnalysis,
0.80,
"static analysis found dependency",
);
edge.add_evidence(
DiscoverySource::ApiTraffic,
0.70,
"API traffic confirms runtime dependency",
);
assert!((edge.confidence - 0.94).abs() < 0.01);
assert_eq!(edge.evidence.len(), 2);
}
#[test]
fn test_confidence_tier() {
assert_eq!(ConfidenceTier::from_score(0.90), ConfidenceTier::High);
assert_eq!(ConfidenceTier::from_score(0.72), ConfidenceTier::Medium);
assert_eq!(ConfidenceTier::from_score(0.45), ConfidenceTier::Low);
}
}