Skip to main content

agentstatedeveloper_core/
edge_confidence.rs

1//! Confidence for call-graph edges (Plan competitive-harvest t-013).
2//!
3//! Static call edges are stored as bare `symbol_id` lists with no confidence.
4//! This adds a sidecar layer: each edge accumulates runtime confirmations (the
5//! `asd` tracer observing the caller actually invoke the callee) on top of a
6//! static prior, and a confidence is derived the same way effect confidence is
7//! in t-001 — reusing [`RuntimeEvidence::derive_confidence`].
8//!
9//! Two provenances:
10//!   - **static-known** — the edge is in the static call graph (prior
11//!     [`STATIC_PRIOR`]); runtime confirmation raises it, absence never lowers
12//!     it (a path simply not exercised is not evidence the edge is wrong).
13//!   - **runtime-only** — the tracer observed a call the static walker missed
14//!     (dynamic dispatch, reflection, callbacks). It definitely happened, so it
15//!     starts at [`RUNTIME_ONLY_PRIOR`] and confirmations push it up fast. These
16//!     are the high-value rows: real edges absent from the static graph.
17//!
18//! The sidecar lives at `/asd/v1/index/edge-evidence/<from>/<to>`, leaving the
19//! hot-path `callees`/`callers` lists untouched.
20
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23
24use crate::schema::RuntimeEvidence;
25
26/// Prior confidence for an edge present in the static call graph.
27pub const STATIC_PRIOR: f64 = 0.7;
28/// Prior for an edge seen only at runtime (missed by static resolution).
29pub const RUNTIME_ONLY_PRIOR: f64 = 0.5;
30
31/// Accumulated evidence for one directed call edge.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct EdgeEvidence {
34    pub from_symbol_id: String,
35    pub to_symbol_id: String,
36    /// Whether the static call graph contains this edge.
37    pub static_known: bool,
38    /// Runtime observations of the call.
39    pub confirmations: u64,
40    /// Frozen prior the confirmations update.
41    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    /// A fresh record for a static-graph edge (no runtime evidence yet).
50    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    /// A fresh record for an edge first seen at runtime (missed by static).
63    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    /// Record one runtime observation of this edge.
76    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    /// If a static edge is later confirmed at runtime, mark it known (a
83    /// runtime-only edge that also turns out to be static).
84    pub fn mark_static_known(&mut self) {
85        if !self.static_known {
86            self.static_known = true;
87            // Lift the prior to the static baseline if it was the lower
88            // runtime-only prior, but never lower an already-higher prior.
89            self.prior = self.prior.max(STATIC_PRIOR);
90        }
91    }
92
93    /// Derived confidence in `[0, 1]` — the same Laplace/Beta derivation as
94    /// effect confidence (edges only ever accumulate confirmations, never
95    /// contradictions, since absence is not evidence against an edge).
96    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        // Idempotent and never lowers a higher prior.
154        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}