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}