Skip to main content

meta_ast/graph/
edge.rs

1//! Edge types for the dependency graph.
2//!
3//! Defines the semantic relationships between nodes in the code graph.
4//! Edges are directed and carry metadata about the relationship type
5//! and confidence level.
6//!
7//! ## Design: uni-directional edges
8//!
9//! All edges are directed. There is no bidirectional or monitor/link
10//! pattern - an edge from A to B means A depends on B, not that B will
11//! be notified of A's failure. Shared-fate semantics (failure propagation)
12//! are a separate, optional annotation that the deploy layer may add
13//! during cut-edge RPC conversion. This follows the principle from
14//! *A Unified Semantics for Future Erlang* §2.2/§6.2: bidirectional links
15//! are replaced by uni-directional links plus monitors, and supervision
16//! trees can be built from uni-directional links alone.
17
18/// Semantic kind of a directed edge in the code graph.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
20#[non_exhaustive]
21pub enum EdgeKind {
22    /// Ownership edge: File owns/contains a symbol.
23    Ownership,
24
25    /// Import edge: File imports/depends on another file.
26    Import,
27
28    /// Reference edge: Symbol references/uses another symbol.
29    Reference,
30
31    /// Flow edge: DataNode def-use or dataflow relationship.
32    Flow,
33}
34
35/// Confidence ladder for scope-resolved edges.
36///
37/// Own file and direct same-language imports score 1.0. Transitive
38/// same-language imports decay to 0.8. Cross-language imports score 0.6.
39pub const CONFIDENCE_OWN_OR_DIRECT: f32 = 1.0;
40/// Transitive same-language import.
41pub const CONFIDENCE_TRANSITIVE: f32 = 0.8;
42/// Cross-language import.
43pub const CONFIDENCE_CROSS_LANGUAGE: f32 = 0.6;
44
45/// Confidence ladder for MetaCall client-call edges.
46///
47/// Unique load-confirmed calls score 1.0. Multiple load-confirmed calls
48/// score 0.8. Unique global matches score 0.6. Multiple global matches
49/// score 0.5. Computed names cap at 0.4.
50pub const CONFIDENCE_CLIENT_UNIQUE_LOAD: f32 = 1.0;
51/// Multiple load-confirmed candidates.
52pub const CONFIDENCE_CLIENT_MULTI_LOAD: f32 = 0.8;
53/// Unique global fallback match.
54pub const CONFIDENCE_CLIENT_UNIQUE_GLOBAL: f32 = 0.6;
55/// Multiple global fallback matches.
56pub const CONFIDENCE_CLIENT_MULTI_GLOBAL: f32 = 0.5;
57/// Computed function, tag, or script name.
58pub const CONFIDENCE_COMPUTED: f32 = 0.4;
59
60/// Dataflow def-use edge confidence.
61pub const CONFIDENCE_DEF_USE: f32 = 0.9;
62
63impl EdgeKind {
64    /// Returns true if this edge kind participates in SCC computation.
65    pub fn participates_in_scc(self) -> bool {
66        matches!(self, EdgeKind::Import | EdgeKind::Reference)
67    }
68
69    /// Returns true if this edge represents a cross-file dependency.
70    pub fn is_cross_file(self) -> bool {
71        matches!(self, EdgeKind::Import)
72    }
73
74    /// Returns the human-readable name of this edge kind.
75    pub const fn as_str(self) -> &'static str {
76        match self {
77            EdgeKind::Ownership => "ownership",
78            EdgeKind::Import => "import",
79            EdgeKind::Reference => "reference",
80            EdgeKind::Flow => "flow",
81        }
82    }
83}
84
85impl std::fmt::Display for EdgeKind {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(f, "{}", self.as_str())
88    }
89}
90
91/// Data stored for each edge in the graph.
92#[derive(Debug, Clone, Copy, serde::Serialize)]
93pub struct EdgeData {
94    /// Semantic kind of the relationship.
95    pub kind: EdgeKind,
96
97    /// Confidence level for the edge resolution.
98    /// Used for cross-language and best-effort resolution.
99    pub confidence: f32,
100
101    /// Flow kind for dataflow edges (None for non-Flow edges).
102    pub flow_kind: Option<crate::model::FlowKind>,
103}
104
105impl EdgeData {
106    /// Creates a new edge with full confidence (1.0) and no flow kind.
107    pub fn new(kind: EdgeKind) -> Self {
108        Self {
109            kind,
110            confidence: 1.0,
111            flow_kind: None,
112        }
113    }
114
115    /// Creates a new edge with specified confidence and no flow kind.
116    pub fn with_confidence(kind: EdgeKind, confidence: f32) -> Self {
117        Self {
118            kind,
119            confidence: confidence.clamp(0.0, 1.0),
120            flow_kind: None,
121        }
122    }
123
124    /// Creates a Flow edge with a flow kind and confidence.
125    pub fn flow(flow_kind: crate::model::FlowKind, confidence: f32) -> Self {
126        Self {
127            kind: EdgeKind::Flow,
128            confidence: confidence.clamp(0.0, 1.0),
129            flow_kind: Some(flow_kind),
130        }
131    }
132
133    pub fn participates_in_scc(&self) -> bool {
134        self.kind.participates_in_scc()
135    }
136}
137
138impl Default for EdgeData {
139    fn default() -> Self {
140        Self::new(EdgeKind::Reference)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn edge_kind_scc_participation() {
150        assert!(!EdgeKind::Ownership.participates_in_scc());
151        assert!(EdgeKind::Import.participates_in_scc());
152        assert!(EdgeKind::Reference.participates_in_scc());
153    }
154
155    #[test]
156    fn edge_kind_as_str() {
157        assert_eq!(EdgeKind::Ownership.as_str(), "ownership");
158        assert_eq!(EdgeKind::Import.as_str(), "import");
159        assert_eq!(EdgeKind::Reference.as_str(), "reference");
160        assert_eq!(EdgeKind::Flow.as_str(), "flow");
161    }
162
163    #[test]
164    fn flow_edge_excluded_from_scc() {
165        assert!(!EdgeKind::Flow.participates_in_scc());
166    }
167
168    #[test]
169    fn flow_edge_not_cross_file() {
170        assert!(!EdgeKind::Flow.is_cross_file());
171    }
172
173    #[test]
174    fn edge_kind_display() {
175        assert_eq!(format!("{}", EdgeKind::Import), "import");
176    }
177
178    #[test]
179    fn edge_data_new_defaults_to_full_confidence() {
180        let edge = EdgeData::new(EdgeKind::Import);
181        assert_eq!(edge.kind, EdgeKind::Import);
182        assert_eq!(edge.confidence, 1.0);
183        assert!(edge.participates_in_scc());
184    }
185
186    #[test]
187    fn edge_data_with_confidence_clamps() {
188        let low = EdgeData::with_confidence(EdgeKind::Reference, -0.5);
189        assert_eq!(low.confidence, 0.0);
190
191        let high = EdgeData::with_confidence(EdgeKind::Reference, 1.5);
192        assert_eq!(high.confidence, 1.0);
193
194        let mid = EdgeData::with_confidence(EdgeKind::Reference, 0.75);
195        assert_eq!(mid.confidence, 0.75);
196    }
197
198    #[test]
199    fn edge_data_default() {
200        let edge: EdgeData = Default::default();
201        assert_eq!(edge.confidence, 1.0);
202        assert!(edge.participates_in_scc());
203    }
204}