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}