Skip to main content

sinter_core/
edge.rs

1use serde::{Deserialize, Serialize};
2
3use crate::node::{NodeId, Span};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
6pub enum Relation {
7    Calls,
8    Uses,
9    Imports,
10    Contains,
11    Implements,
12    Extends,
13}
14
15impl Relation {
16    pub fn as_str(self) -> &'static str {
17        match self {
18            Self::Calls => "calls",
19            Self::Uses => "uses",
20            Self::Imports => "imports",
21            Self::Contains => "contains",
22            Self::Implements => "implements",
23            Self::Extends => "extends",
24        }
25    }
26}
27
28/// What binds this edge to its target (R2: evidence or nothing).
29/// Global name uniqueness is not evidence and has no variant here.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
31pub enum Evidence {
32    /// Syntactic containment seen directly in the parse tree.
33    Structural,
34    /// Name visible in the reference's own file scope.
35    Scope,
36    /// An import statement binds the reference's path to the target.
37    Import,
38    /// A compiler-produced SCIP index binds reference to definition.
39    Scip,
40    /// An operator-declared binding from a workspace manifest (runtime
41    /// coupling like queue topics/HTTP routes that no static analysis can
42    /// see). Auditable in the manifest; never inferred.
43    Declared,
44    /// Dynamic-dispatch fan-out: a trait/interface method is assumed to
45    /// reach every implementation in the corpus. Conservative
46    /// over-approximation, deliberately excludable (`--certain`,
47    /// `--evidence` without "dynamic").
48    Dynamic,
49}
50
51impl Evidence {
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::Structural => "structural",
55            Self::Scope => "scope",
56            Self::Import => "import",
57            Self::Scip => "scip",
58            Self::Declared => "declared",
59            Self::Dynamic => "dynamic",
60        }
61    }
62
63    /// Compiler-grade evidence is certain; heuristic-free but indirect
64    /// evidence (scope/import matching) is inferred.
65    pub fn confidence(self) -> Confidence {
66        match self {
67            Self::Structural | Self::Scip | Self::Declared => Confidence::Certain,
68            Self::Scope | Self::Import | Self::Dynamic => Confidence::Inferred,
69        }
70    }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
74pub enum Confidence {
75    Certain,
76    Inferred,
77}
78
79/// Directed edge `src -> dst`. The graph is a multigraph: parallel edges
80/// that differ in relation, evidence, or confidence coexist.
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
82pub struct Edge {
83    pub src: NodeId,
84    pub dst: NodeId,
85    pub relation: Relation,
86    pub evidence: Evidence,
87    pub confidence: Confidence,
88    /// Byte span of the binding reference in the src node's file (the file
89    /// is derivable from `src`). None when no single site exists:
90    /// containment, dynamic fan-out, implements/extends pairing, declared
91    /// links. When several sites bind the same (src, dst, relation,
92    /// evidence), storage keeps ONE representative site — the field is
93    /// last so identity orders before site.
94    pub site: Option<Span>,
95}
96
97impl Edge {
98    /// Identity without the site: two edges equal here are the same
99    /// dependency fact observed at (possibly) different call sites.
100    pub fn identity(&self) -> (&NodeId, &NodeId, Relation, Evidence, Confidence) {
101        (
102            &self.src,
103            &self.dst,
104            self.relation,
105            self.evidence,
106            self.confidence,
107        )
108    }
109}