Skip to main content

devscout_rs/graph/
edge.rs

1use serde::{Deserialize, Serialize};
2
3use super::fragment_types::is_false;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6/// Represents `Candidate`.
7pub struct Candidate {
8    /// The id value.
9    pub id: String,
10    /// The file value.
11    pub file: String,
12}
13
14/// Which heuristic tier emitted a guess.
15///
16/// The two differ by an order of magnitude in precision and `heuristic: true`
17/// alone cannot tell them apart: `Ext` is C#'s own extension-method lookup run over a recorded
18/// `(member, this-type)` bucket -- a real rule, only unverifiable against an
19/// out-of-graph receiver -- while `Guess` is the scored tier picking by NAME
20/// among the defs that happen to declare a member so called.
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum HeuristicTier {
24    /// Tier (f): extension-method lookup.
25    Ext,
26    /// The scored tier: a name guess.
27    Guess,
28}
29
30/// The guess tag, appended LAST on every edge kind that can
31/// carry it. `heuristic: true` is set only on an edge a heuristic tier
32/// emitted and never writes `heuristic: false`, so this side pairs
33/// `skip_serializing_if` with `default`: absent == precise, and a precise
34/// edge's bytes are exactly what they were before the tag existed. Only
35/// the resolver's heuristic tiers set it today (both
36/// `uses-member`), but the flag lives on all three targeted kinds because
37/// the query layer's heuristic adjacency is kind-keyed, so a future tier
38/// tagging a `uses-type` edge needs no schema change.
39///
40/// `uses-member` carries two more appended keys, `tier` then `member`, in
41/// that order and NOT mirrored onto the other two kinds: neither has a
42/// member to name, and no tier tags one today. `tier` splits the umbrella
43/// flag into the two tiers a reader can act on differently; `member` names
44/// the member the reference reads or calls and is written on every
45/// uses-member edge, precise ones included, because "which member" is a fact
46/// about the reference rather than about the guess. Both are
47/// omit-when-`None`, so a reader of the old shape sees only added keys.
48///
49/// Fills the slot this module's `Edge::UsesMember::source` reserved: which of
50/// two disjoint populations a compiler-vouched edge belongs to. `Semantic`
51/// stands in place of whatever the ladder would have produced for the SAME
52/// reference the extractor already emitted (a per-reference override, one for
53/// one). `SemanticDiscovered` has no extractor reference at all -- the site
54/// exists only because the admitted artifact named it -- so it is never
55/// counted against the extractor-emitted population in `edges_by_kind` or the
56/// syntax-lane audit denominator; only the audit's own `tiers.semantic` block
57/// reports it, and separately from `Semantic`.
58#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub enum SemanticProvenance {
61    /// A same-context compiler fact confirmed or overrode the extractor's own
62    /// reference at that occurrence.
63    Semantic,
64    /// A compiler-verified occurrence the syntax extractor emitted no
65    /// reference for.
66    SemanticDiscovered,
67}
68
69/// Fills the slot this variant previously reserved: `source:
70/// Option<SemanticProvenance>`, appended after `member` and omitted when
71/// empty, on the same rule.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73#[serde(tag = "kind")]
74pub enum Edge {
75    #[serde(rename = "inherits")]
76    /// Represents `Inherits`.
77    Inherits {
78        /// The value value.
79        from_file: String,
80        /// The value value.
81        from_line: usize,
82        /// The value value.
83        to: String,
84        /// The value value.
85        to_file: String,
86        #[serde(default, skip_serializing_if = "is_false")]
87        /// The value value.
88        heuristic: bool,
89    },
90    #[serde(rename = "uses-type")]
91    /// Represents `UsesType`.
92    UsesType {
93        /// The value value.
94        from_file: String,
95        /// The value value.
96        from_line: usize,
97        /// The value value.
98        to: String,
99        /// The value value.
100        to_file: String,
101        #[serde(default, skip_serializing_if = "is_false")]
102        /// The value value.
103        heuristic: bool,
104    },
105    #[serde(rename = "uses-member")]
106    /// Represents `UsesMember`.
107    UsesMember {
108        /// The value value.
109        from_file: String,
110        /// The value value.
111        from_line: usize,
112        /// The value value.
113        to: String,
114        /// The value value.
115        to_file: String,
116        #[serde(default, skip_serializing_if = "is_false")]
117        /// The value value.
118        heuristic: bool,
119        #[serde(default, skip_serializing_if = "Option::is_none")]
120        /// Which guess tier emitted this edge -- `Some` exactly when
121        /// `heuristic` is true. Build the variant through
122        /// `Edge::uses_member`, which derives one from the other.
123        tier: Option<HeuristicTier>,
124        #[serde(default, skip_serializing_if = "Option::is_none")]
125        /// The member this reference reads or calls, on precise and
126        /// heuristic edges alike. `None` only for a reference the extractor
127        /// recorded no member name for.
128        member: Option<String>,
129        #[serde(default, skip_serializing_if = "Option::is_none")]
130        /// `Some` exactly on an edge a same-context compiler fact produced
131        /// (`Edge::uses_member_semantic`); `None`, and hence entirely absent
132        /// from the serialized bytes, on every edge the ladder or a
133        /// heuristic tier produced -- a graph built without the semantic
134        /// layer is byte-identical to one built before this field existed.
135        source: Option<SemanticProvenance>,
136        #[serde(default, skip_serializing_if = "Option::is_none")]
137        /// The admitted occurrence's own overload signature (the producer's
138        /// `target.overloadSignature`, e.g. `"(bool)->void"`), appended LAST
139        /// -- present only alongside `source`, and only when the admitted
140        /// occurrence named one. Two same-line overloads of one member on
141        /// the SAME type resolve to the SAME `to`/`to_file` (the type-level
142        /// identity a `uses-member` edge already carries), so without this
143        /// field their projected edges would be byte-identical and a reader
144        /// could never tell them apart as the two distinct facts they are.
145        /// `None` on every syntax or heuristic edge, which keeps a graph
146        /// built without the semantic layer byte-identical to one built
147        /// before this field existed.
148        overload_signature: Option<String>,
149    },
150    #[serde(rename = "imports")]
151    /// The value value.
152    Imports {
153        /// The source file.
154        from_file: String,
155        /// The source line.
156        from_line: usize,
157        /// The imported target.
158        target: String,
159    },
160    #[serde(rename = "ambiguous")]
161    /// Represents `Ambiguous`.
162    Ambiguous {
163        /// The value value.
164        origin: String,
165        /// The value value.
166        from_file: String,
167        /// The value value.
168        from_line: usize,
169        /// The value value.
170        raw: String,
171        /// The value value.
172        candidates: Vec<Candidate>,
173        /// The value value.
174        candidate_count: usize,
175    },
176    /// Constructor-parameter DI resolution (see resolve.rs's
177    /// `resolve_ctor_param`). Field order (`kind`, `from_file`, `from_line`,
178    /// `iface`, `resolution`, `args`, `to`, `candidates`) is significant --
179    /// it fixes the serialized field order.
180    /// `iface` is the injected type's bare identifier; `args` its closed
181    /// generic arguments, present only when it has any; `to` the resolved
182    /// implementation's def id, present only for 'plain'/'closed'/
183    /// 'open-generic'; `candidates` the capped, sorted tie list, present only
184    /// for 'ambiguous'.
185    #[serde(rename = "ctor-di")]
186    CtorDi {
187        /// The value value.
188        from_file: String,
189        /// The value value.
190        from_line: usize,
191        /// The value value.
192        iface: String,
193        /// The value value.
194        resolution: String,
195        #[serde(default, skip_serializing_if = "Option::is_none")]
196        /// The value value.
197        args: Option<Vec<String>>,
198        #[serde(default, skip_serializing_if = "Option::is_none")]
199        /// The value value.
200        to: Option<String>,
201        #[serde(default, skip_serializing_if = "Vec::is_empty")]
202        /// The value value.
203        candidates: Vec<Candidate>,
204    },
205    /// A TS/TSX module import. Distinct from `imports` (C#'s
206    /// `using` directive, which names a NAMESPACE and resolves to no file):
207    /// `target` is the specifier as written and `to_file` the file it
208    /// resolved to. `via` is appended LAST and present only on a
209    /// barrel-followed edge, naming the module the source literally imported
210    /// so a reader can tell the routing table apart from the dependency.
211    #[serde(rename = "import")]
212    Import {
213        /// The value value.
214        from_file: String,
215        /// The value value.
216        from_line: usize,
217        /// The value value.
218        target: String,
219        /// The value value.
220        to_file: String,
221        #[serde(default, skip_serializing_if = "Option::is_none")]
222        /// The value value.
223        via: Option<String>,
224    },
225    /// A call or `new` naming an imported or locally-exported
226    /// declaration.
227    #[serde(rename = "call")]
228    /// The value value.
229    Call {
230        /// The source file.
231        from_file: String,
232        /// The source line.
233        from_line: usize,
234        /// The target definition identifier.
235        to: String,
236        /// The target file.
237        to_file: String,
238    },
239    /// A JSX tag naming a component declaration.
240    #[serde(rename = "jsx-use")]
241    /// The value value.
242    JsxUse {
243        /// The source file.
244        from_file: String,
245        /// The source line.
246        from_line: usize,
247        /// The target definition identifier.
248        to: String,
249        /// The target file.
250        to_file: String,
251    },
252    /// An action creator or thunk handed to a dispatching call
253    /// (`dispatch(...)`, `ofType(...)`).
254    #[serde(rename = "dispatch")]
255    /// The value value.
256    Dispatch {
257        /// The source file.
258        from_file: String,
259        /// The source line.
260        from_line: usize,
261        /// The target definition identifier.
262        to: String,
263        /// The target file.
264        to_file: String,
265    },
266    /// The implementation of a service interface: either a type-level fact
267    /// (`member` absent) read off a two-type-argument DI registration call
268    /// (`services.AddScoped<IContract, Impl>()`), recorded against the
269    /// IMPLEMENTATION type's own declaring file and line -- the same
270    /// convention `inherits` uses for its own base-list reference, applied
271    /// here to a fact the resolver derived rather than one it read directly
272    /// off that type's own base list -- or a member-level fact (`member`
273    /// present, naming the satisfied interface member) connecting an
274    /// implementing member to the interface member it satisfies. Never a
275    /// guess: an ambiguous name-and-arity match emits nothing rather than
276    /// picking a candidate. `to`/`to_file` name the interface.
277    #[serde(rename = "implements")]
278    Implements {
279        /// The value value.
280        from_file: String,
281        /// The value value.
282        from_line: usize,
283        /// The value value.
284        to: String,
285        /// The value value.
286        to_file: String,
287        #[serde(default, skip_serializing_if = "Option::is_none")]
288        /// The interface member this implementation satisfies; absent for
289        /// the type-level, registration-driven fact.
290        member: Option<String>,
291    },
292    /// An `override` member connected to the nearest in-graph base member of
293    /// the same name and arity, matched the same way `implements` is: two or
294    /// more candidates at that arity emit nothing. `to`/`to_file` name the
295    /// base type declaring the overridden member; `member` names it.
296    #[serde(rename = "overrides")]
297    Overrides {
298        /// The value value.
299        from_file: String,
300        /// The value value.
301        from_line: usize,
302        /// The value value.
303        to: String,
304        /// The value value.
305        to_file: String,
306        #[serde(default, skip_serializing_if = "Option::is_none")]
307        /// The value value.
308        member: Option<String>,
309    },
310    /// A message-bus hop: a publish site whose resolved message reaches a
311    /// registered consumer/handler def, both ends resolved through the
312    /// ordinary ladder in their own file's context, never a bare-name or
313    /// same-line match. `to`/`to_file` name the consumer; `message` names
314    /// the resolved message def's own id, followed by an array suffix
315    /// (`[]` per array layer) when the identity both sides agreed on is an
316    /// array of that def rather than the def alone -- never the raw text
317    /// either site wrote it as, so two same-named messages in different
318    /// namespaces, or a message and an array of it, can never be
319    /// conflated. `evidence` names which consumer shape the hop was
320    /// established through -- see `resolve/bus.rs`'s own closed word list,
321    /// the one place any of its words is spelled.
322    #[serde(rename = "bus-hop")]
323    BusHop {
324        /// The value value.
325        from_file: String,
326        /// The value value.
327        from_line: usize,
328        /// The value value.
329        message: String,
330        /// The value value.
331        to: String,
332        /// The value value.
333        to_file: String,
334        /// The value value.
335        evidence: String,
336    },
337}
338
339impl Edge {
340    /// The guess tier that emitted this edge, or `None` on a precise one --
341    /// and on every kind that carries no tier at all, which is every kind but
342    /// `uses-member`.
343    pub fn tier(&self) -> Option<HeuristicTier> {
344        match self {
345            Edge::UsesMember { tier, .. } => *tier,
346            _ => None,
347        }
348    }
349
350    /// Whether the edge declares itself a guess, across the three kinds that
351    /// can carry the flag.
352    pub fn is_heuristic(&self) -> bool {
353        match self {
354            Edge::Inherits { heuristic, .. }
355            | Edge::UsesType { heuristic, .. }
356            | Edge::UsesMember { heuristic, .. } => *heuristic,
357            _ => false,
358        }
359    }
360
361    /// The one way to build a `uses-member` edge. `heuristic` is DERIVED from
362    /// `tier` rather than passed alongside it, so the two cannot disagree: a
363    /// tagged edge is always flagged, a flagged edge always names its tier,
364    /// and no emit site can grow a third state by forgetting a field.
365    pub fn uses_member(
366        from_file: String,
367        from_line: usize,
368        to: String,
369        to_file: String,
370        member: Option<String>,
371        tier: Option<HeuristicTier>,
372    ) -> Edge {
373        Edge::UsesMember {
374            from_file,
375            from_line,
376            to,
377            to_file,
378            heuristic: tier.is_some(),
379            tier,
380            member,
381            source: None,
382            overload_signature: None,
383        }
384    }
385
386    /// The one way to build a compiler-vouched `uses-member` edge: never
387    /// heuristic, never tiered (a confirmed compiler fact is not a guess),
388    /// always carrying `source` so a reader can tell it apart from every
389    /// edge the ladder or a heuristic tier produced. `overload_signature`
390    /// carries the admitted occurrence's own signature, when it named one --
391    /// the exact identity that keeps two same-line overloads of one member
392    /// distinguishable once projected.
393    pub fn uses_member_semantic(
394        from_file: String,
395        from_line: usize,
396        to: String,
397        to_file: String,
398        member: Option<String>,
399        provenance: SemanticProvenance,
400        overload_signature: Option<String>,
401    ) -> Edge {
402        Edge::UsesMember {
403            from_file,
404            from_line,
405            to,
406            to_file,
407            heuristic: false,
408            tier: None,
409            member,
410            source: Some(provenance),
411            overload_signature,
412        }
413    }
414
415    /// Which population produced this edge, when it names one at all --
416    /// `None` for every kind that is not a compiler-vouched `uses-member`
417    /// edge, including an ordinary precise or heuristic one.
418    pub fn provenance(&self) -> Option<SemanticProvenance> {
419        match self {
420            Edge::UsesMember { source, .. } => *source,
421            _ => None,
422        }
423    }
424}
425
426#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
427/// Represents `EdgesByKind`.
428pub struct EdgesByKind {
429    /// The inherits value.
430    pub inherits: usize,
431    #[serde(rename = "uses-type")]
432    /// The uses type value.
433    pub uses_type: usize,
434    /// The imports value.
435    pub imports: usize,
436    #[serde(rename = "uses-member")]
437    /// The uses member value.
438    pub uses_member: usize,
439    /// Appended LAST, matching the `ctor-di` slot in the fixed key order.
440    #[serde(rename = "ctor-di")]
441    pub ctor_di: usize,
442    /// The type-level and member-level `implements` edges, counted
443    /// together -- appended after `ctor-di`, before the optional TS keys.
444    #[serde(default)]
445    pub implements: usize,
446    /// The member-level `overrides` edges, appended right after
447    /// `implements`, same rule.
448    #[serde(default)]
449    pub overrides: usize,
450    /// The four TS edge counts, appended after `overrides` in this exact order
451    /// and ONLY when the repo carries a TS fragment at all. A C#-only repo's
452    /// stats block omits them entirely -- which is why these are `Option` and
453    /// not a plain `0`.
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub import: Option<usize>,
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    /// The call value.
458    pub call: Option<usize>,
459    #[serde(rename = "jsx-use", default, skip_serializing_if = "Option::is_none")]
460    /// The jsx use value.
461    pub jsx_use: Option<usize>,
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    /// The dispatch value.
464    pub dispatch: Option<usize>,
465    /// The message-bus `bus-hop` edges, appended LAST, after `dispatch`.
466    /// `None` (never `Some(0)`) for a repository with no bus hop at all, so
467    /// a bus-free graph's stats block serializes exactly as it did before
468    /// this kind existed.
469    #[serde(rename = "bus-hop", default, skip_serializing_if = "Option::is_none")]
470    pub bus_hop: Option<usize>,
471}
472
473/// `heuristic_edge_count` split by the tier that emitted each edge, in the
474/// fixed key order every tier-keyed output uses (ext, then guess).
475///
476/// Both keys are always written, and `ext + guess` equals `heuristic_edge_count` --
477/// including after the heuristic-side dedup, which decrements the dropped
478/// edge's own tier.
479#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
480pub struct HeuristicByTier {
481    /// Edges tier (f) emitted.
482    pub ext: usize,
483    /// Edges the scored tier emitted.
484    pub guess: usize,
485}
486
487/// The resolve-time consumption path's own run-level counters: appended to
488/// `Stats` only when the semantic layer actually loaded for this run, so a
489/// syntax-only build (no admitted artifact, or a stale one) carries no
490/// `semantic` key at all -- the same omit-when-absent rule every
491/// enrichment-layer fact follows, and what keeps a graph built without the layer
492/// byte-identical to one built before it existed.
493#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
494pub struct SemanticStats {
495    /// Per-reference overrides the same-context authority rule confirmed.
496    pub confirmed: usize,
497    /// How many of those confirmations displaced a different target the
498    /// ladder itself had already bound -- the disagreement diagnostic count.
499    pub disagreements: usize,
500    /// Compiler-discovered edges: admitted, confirmed occurrences the
501    /// extractor emitted no reference for at all. Never pooled with
502    /// `confirmed` in any count this struct reports.
503    pub discovered: usize,
504}