agentstatedeveloper_core/
edge_confidence.rs1use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23
24use crate::schema::RuntimeEvidence;
25
26pub const STATIC_PRIOR: f64 = 0.7;
28pub const RUNTIME_ONLY_PRIOR: f64 = 0.5;
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct EdgeEvidence {
34 pub from_symbol_id: String,
35 pub to_symbol_id: String,
36 pub static_known: bool,
38 pub confirmations: u64,
40 pub prior: f64,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub last_trace_id: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub last_observed_at: Option<DateTime<Utc>>,
46}
47
48impl EdgeEvidence {
49 pub fn static_edge(from: &str, to: &str) -> Self {
51 Self {
52 from_symbol_id: from.to_string(),
53 to_symbol_id: to.to_string(),
54 static_known: true,
55 confirmations: 0,
56 prior: STATIC_PRIOR,
57 last_trace_id: None,
58 last_observed_at: None,
59 }
60 }
61
62 pub fn runtime_only_edge(from: &str, to: &str) -> Self {
64 Self {
65 from_symbol_id: from.to_string(),
66 to_symbol_id: to.to_string(),
67 static_known: false,
68 confirmations: 0,
69 prior: RUNTIME_ONLY_PRIOR,
70 last_trace_id: None,
71 last_observed_at: None,
72 }
73 }
74
75 pub fn confirm(&mut self, trace_id: &str, at: DateTime<Utc>) {
77 self.confirmations += 1;
78 self.last_trace_id = Some(trace_id.to_string());
79 self.last_observed_at = Some(at);
80 }
81
82 pub fn mark_static_known(&mut self) {
85 if !self.static_known {
86 self.static_known = true;
87 self.prior = self.prior.max(STATIC_PRIOR);
90 }
91 }
92
93 pub fn confidence(&self) -> f64 {
97 RuntimeEvidence::derive_confidence(self.prior, self.confirmations, 0)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 fn ts() -> DateTime<Utc> {
106 DateTime::parse_from_rfc3339("2026-06-29T00:00:00Z")
107 .unwrap()
108 .with_timezone(&Utc)
109 }
110
111 #[test]
112 fn fresh_static_edge_sits_at_static_prior() {
113 let e = EdgeEvidence::static_edge("a", "b");
114 assert!(e.static_known);
115 assert!((e.confidence() - STATIC_PRIOR).abs() < 1e-9);
116 }
117
118 #[test]
119 fn runtime_only_edge_starts_lower_and_is_flagged() {
120 let e = EdgeEvidence::runtime_only_edge("a", "b");
121 assert!(!e.static_known);
122 assert!((e.confidence() - RUNTIME_ONLY_PRIOR).abs() < 1e-9);
123 }
124
125 #[test]
126 fn confirmations_raise_confidence_monotonically() {
127 let mut e = EdgeEvidence::static_edge("a", "b");
128 let base = e.confidence();
129 e.confirm("trc_1", ts());
130 let one = e.confidence();
131 e.confirm("trc_2", ts());
132 let two = e.confidence();
133 assert!(one > base && two > one, "{base} {one} {two}");
134 assert_eq!(e.confirmations, 2);
135 assert_eq!(e.last_trace_id.as_deref(), Some("trc_2"));
136 }
137
138 #[test]
139 fn confidence_never_exceeds_one() {
140 let mut e = EdgeEvidence::runtime_only_edge("a", "b");
141 for _ in 0..10_000 {
142 e.confirm("t", ts());
143 }
144 assert!(e.confidence() < 1.0 && e.confidence() > 0.99);
145 }
146
147 #[test]
148 fn mark_static_known_lifts_prior_without_lowering() {
149 let mut e = EdgeEvidence::runtime_only_edge("a", "b");
150 e.mark_static_known();
151 assert!(e.static_known);
152 assert!((e.prior - STATIC_PRIOR).abs() < 1e-9);
153 let mut high = EdgeEvidence::static_edge("a", "b");
155 high.prior = 0.9;
156 high.mark_static_known();
157 assert!((high.prior - 0.9).abs() < 1e-9);
158 }
159}