Skip to main content

sqry_core/graph/unified/
traversal.rs

1//! Shared traversal result types with `EdgeClassification` and `MaterializedEdge`.
2//!
3//! These types form the universal output contract for all BFS traversals in sqry.
4//! Consumer crates (LSP, MCP, CLI) convert `TraversalResult` into their
5//! protocol-specific response types.
6
7#[cfg(test)]
8use super::edge::kind::ResolvedVia;
9use super::edge::kind::{EdgeKind, ExportKind};
10use super::materialize::MaterializedNode;
11
12/// Classification of an edge's semantic intent with preserved metadata.
13///
14/// Provides a coarse categorization of `EdgeKind` variants for consumers that
15/// do not need the full edge semantics. The `From<&EdgeKind>` conversion is
16/// exhaustive — no wildcard fallback — so future `EdgeKind` additions produce
17/// a compile error forcing conscious classification.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum EdgeClassification {
20    /// Function/method call (includes trait method bindings).
21    Call {
22        /// Whether the call is async (awaited).
23        is_async: bool,
24        /// Whether the call crosses a language or service boundary.
25        is_cross_boundary: bool,
26    },
27    /// Import statement.
28    Import {
29        /// Whether this is a wildcard import.
30        is_wildcard: bool,
31    },
32    /// Export statement.
33    Export {
34        /// Whether this is a re-export.
35        is_reexport: bool,
36    },
37    /// Symbol reference.
38    Reference,
39    /// Class/trait inheritance.
40    Inherits,
41    /// Interface/trait implementation.
42    Implements,
43    /// Structural containment.
44    Contains,
45    /// Symbol definition.
46    Defines,
47    /// Type annotation/association.
48    TypeOf,
49    /// Database access (queries, reads, writes, triggers).
50    DatabaseAccess,
51    /// Service-level interaction (message queues, websockets, gRPC, etc.).
52    ServiceInteraction,
53}
54
55impl From<&EdgeKind> for EdgeClassification {
56    // Arms are grouped by semantic domain (calls, imports, OOP, JVM classpath, etc.)
57    // even when multiple domains map to the same classification variant.
58    #[allow(clippy::match_same_arms)]
59    fn from(kind: &EdgeKind) -> Self {
60        match kind {
61            // ---- Calls ----
62            EdgeKind::Calls { is_async, .. } => Self::Call {
63                is_async: *is_async,
64                is_cross_boundary: false,
65            },
66            EdgeKind::TraitMethodBinding { .. } => Self::Call {
67                is_async: false,
68                is_cross_boundary: false,
69            },
70            EdgeKind::FfiCall { .. }
71            | EdgeKind::HttpRequest { .. }
72            | EdgeKind::GrpcCall { .. }
73            | EdgeKind::WebAssemblyCall => Self::Call {
74                is_async: false,
75                is_cross_boundary: true,
76            },
77
78            // ---- Imports / Exports ----
79            EdgeKind::Imports { is_wildcard, .. } => Self::Import {
80                is_wildcard: *is_wildcard,
81            },
82            EdgeKind::Exports { kind, .. } => Self::Export {
83                is_reexport: matches!(kind, ExportKind::Reexport),
84            },
85
86            // ---- References ----
87            EdgeKind::References => Self::Reference,
88
89            // ---- OOP ----
90            EdgeKind::Inherits | EdgeKind::SealedPermit => Self::Inherits,
91            EdgeKind::Implements => Self::Implements,
92
93            // ---- Structural ----
94            EdgeKind::Contains | EdgeKind::CompanionOf => Self::Contains,
95            EdgeKind::Defines => Self::Defines,
96
97            // ---- Type ----
98            EdgeKind::TypeOf { .. } => Self::TypeOf,
99
100            // ---- Database ----
101            EdgeKind::DbQuery { .. }
102            | EdgeKind::TableRead { .. }
103            | EdgeKind::TableWrite { .. }
104            | EdgeKind::TriggeredBy { .. } => Self::DatabaseAccess,
105
106            // ---- Service interactions ----
107            EdgeKind::MessageQueue { .. }
108            | EdgeKind::WebSocket { .. }
109            | EdgeKind::GraphQLOperation { .. }
110            | EdgeKind::ProcessExec { .. }
111            | EdgeKind::FileIpc { .. }
112            | EdgeKind::ProtocolCall { .. } => Self::ServiceInteraction,
113
114            // ---- JVM classpath → closest semantic match ----
115            EdgeKind::GenericBound | EdgeKind::TypeArgument => Self::TypeOf,
116            EdgeKind::AnnotatedWith | EdgeKind::AnnotationParam => Self::Reference,
117            EdgeKind::LambdaCaptures | EdgeKind::ExtensionReceiver => Self::Reference,
118            EdgeKind::ModuleExports | EdgeKind::ModuleOpens => Self::Export { is_reexport: false },
119            EdgeKind::ModuleRequires | EdgeKind::ModuleProvides => {
120                Self::Import { is_wildcard: false }
121            }
122
123            // ---- Rust-specific ----
124            EdgeKind::MacroExpansion { .. } => Self::Reference,
125            EdgeKind::LifetimeConstraint { .. } => Self::Reference,
126
127            // ---- T3 error chains (Go) ----
128            // Wraps edges are NOT included in standard callers/callees
129            // results (T3 design §1.3); explicit `Wraps`-aware consumers
130            // walk the edge directly until the Cluster F planner `wraps:`
131            // predicate / Cluster G traversal surfaces land. Mapping to
132            // `Reference` is the closest match in this taxonomy — same
133            // disposition as MacroExpansion / LifetimeConstraint, which
134            // are likewise filtered out of call traversal.
135            EdgeKind::Wraps { .. } => Self::Reference,
136
137            // ---- T2.4 / T2.5 Go channel pairing + generic instantiation ----
138            // Neither edge participates in standard callers/callees traversal
139            // (the `Calls` edge is emitted separately for generic calls, and
140            // channel ops are not calls). Mapping to `Reference` matches the
141            // `Wraps` disposition — a neutral label that keeps these edges out
142            // of call-flow classification while leaving them walkable by
143            // consumers that opt into the kind explicitly.
144            EdgeKind::ChannelPeer { .. } | EdgeKind::Instantiates { .. } => Self::Reference,
145        }
146    }
147}
148
149/// A materialized edge in a traversal result.
150///
151/// Indices reference the `nodes` vector in `TraversalResult`. Both
152/// `source_idx` and `target_idx` are guaranteed to be `< nodes.len()`.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct MaterializedEdge {
155    /// Index into `TraversalResult.nodes` for the source node.
156    pub source_idx: usize,
157    /// Index into `TraversalResult.nodes` for the target node.
158    pub target_idx: usize,
159    /// Semantic classification with preserved metadata.
160    pub classification: EdgeClassification,
161    /// Raw edge kind for consumers needing full semantics (e.g., confidence scoring).
162    pub raw_kind: EdgeKind,
163    /// Traversal depth at which this edge was discovered.
164    pub depth: u32,
165}
166
167/// Why a traversal was truncated.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum TruncationReason {
170    /// Maximum traversal depth reached.
171    DepthLimit,
172    /// Maximum node count reached.
173    NodeLimit,
174    /// Maximum edge count reached.
175    EdgeLimit,
176    /// Maximum path count reached (`trace_path`).
177    PathLimit,
178}
179
180/// Metadata about a completed traversal.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct TraversalMetadata {
183    /// Why the traversal was truncated, if at all.
184    pub truncation: Option<TruncationReason>,
185    /// Whether the max depth bound was reached during traversal.
186    pub max_depth_reached: bool,
187    /// Number of seed nodes the traversal started from.
188    pub seed_count: usize,
189    /// Total nodes visited during traversal (may exceed nodes in result).
190    pub nodes_visited: usize,
191    /// Total materialized nodes in the result.
192    pub total_nodes: usize,
193    /// Total materialized edges in the result.
194    pub total_edges: usize,
195}
196
197/// Universal traversal result that all BFS implementations produce.
198///
199/// # Index Invariants
200///
201/// 1. `nodes` is deduped by `NodeId` — each node appears exactly once.
202/// 2. `nodes` is populated in BFS discovery order (first-seen).
203/// 3. Every `source_idx`, `target_idx`, and path index is `< nodes.len()`.
204/// 4. When any limit triggers, edges and paths referencing truncated nodes are
205///    dropped atomically.
206/// 5. `metadata.truncation` is `Some(reason)` whenever a limit causes pruning.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct TraversalResult {
209    /// Materialized nodes (each carries `node_id: NodeId`).
210    pub nodes: Vec<MaterializedNode>,
211    /// Materialized edges (indices reference `nodes` vector).
212    pub edges: Vec<MaterializedEdge>,
213    /// Optional ordered paths (indices into `nodes` vector).
214    /// Used by `trace_path` for K shortest paths.
215    pub paths: Option<Vec<Vec<usize>>>,
216    /// Traversal metadata.
217    pub metadata: TraversalMetadata,
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::graph::unified::edge::kind::{
224        DbQueryType, ExportKind, FfiConvention, LifetimeConstraintKind, MacroExpansionKind,
225    };
226    use crate::graph::unified::string::id::StringId;
227
228    /// Helper to create a dummy `StringId` for test edge kinds that require one.
229    fn test_string_id() -> StringId {
230        StringId::new(1)
231    }
232
233    #[test]
234    fn calls_async_classification() {
235        let edge = EdgeKind::Calls {
236            argument_count: 2,
237            is_async: true,
238            resolved_via: ResolvedVia::Direct,
239        };
240        let classified = EdgeClassification::from(&edge);
241        assert_eq!(
242            classified,
243            EdgeClassification::Call {
244                is_async: true,
245                is_cross_boundary: false,
246            }
247        );
248    }
249
250    #[test]
251    fn ffi_call_cross_boundary() {
252        let edge = EdgeKind::FfiCall {
253            convention: FfiConvention::C,
254        };
255        let classified = EdgeClassification::from(&edge);
256        assert_eq!(
257            classified,
258            EdgeClassification::Call {
259                is_async: false,
260                is_cross_boundary: true,
261            }
262        );
263    }
264
265    #[test]
266    fn trait_method_binding_classification() {
267        let edge = EdgeKind::TraitMethodBinding {
268            trait_name: test_string_id(),
269            impl_type: test_string_id(),
270            is_ambiguous: false,
271        };
272        let classified = EdgeClassification::from(&edge);
273        assert_eq!(
274            classified,
275            EdgeClassification::Call {
276                is_async: false,
277                is_cross_boundary: false,
278            }
279        );
280    }
281
282    #[test]
283    fn exports_reexport_classification() {
284        let edge = EdgeKind::Exports {
285            kind: ExportKind::Reexport,
286            alias: None,
287        };
288        let classified = EdgeClassification::from(&edge);
289        assert_eq!(classified, EdgeClassification::Export { is_reexport: true });
290    }
291
292    #[test]
293    fn exports_direct_classification() {
294        let edge = EdgeKind::Exports {
295            kind: ExportKind::Direct,
296            alias: None,
297        };
298        let classified = EdgeClassification::from(&edge);
299        assert_eq!(
300            classified,
301            EdgeClassification::Export { is_reexport: false }
302        );
303    }
304
305    #[test]
306    fn sealed_permit_inherits() {
307        let edge = EdgeKind::SealedPermit;
308        let classified = EdgeClassification::from(&edge);
309        assert_eq!(classified, EdgeClassification::Inherits);
310    }
311
312    #[test]
313    fn companion_of_contains() {
314        let edge = EdgeKind::CompanionOf;
315        let classified = EdgeClassification::from(&edge);
316        assert_eq!(classified, EdgeClassification::Contains);
317    }
318
319    #[test]
320    fn generic_bound_type_of() {
321        let edge = EdgeKind::GenericBound;
322        let classified = EdgeClassification::from(&edge);
323        assert_eq!(classified, EdgeClassification::TypeOf);
324    }
325
326    #[test]
327    fn module_exports_export() {
328        let edge = EdgeKind::ModuleExports;
329        let classified = EdgeClassification::from(&edge);
330        assert_eq!(
331            classified,
332            EdgeClassification::Export { is_reexport: false }
333        );
334    }
335
336    #[test]
337    fn http_request_cross_boundary() {
338        let edge = EdgeKind::HttpRequest {
339            method: crate::graph::unified::edge::kind::HttpMethod::Get,
340            url: None,
341        };
342        let classified = EdgeClassification::from(&edge);
343        assert_eq!(
344            classified,
345            EdgeClassification::Call {
346                is_async: false,
347                is_cross_boundary: true,
348            }
349        );
350    }
351
352    #[test]
353    fn db_query_database_access() {
354        let edge = EdgeKind::DbQuery {
355            query_type: DbQueryType::Select,
356            table: None,
357        };
358        let classified = EdgeClassification::from(&edge);
359        assert_eq!(classified, EdgeClassification::DatabaseAccess);
360    }
361
362    #[test]
363    fn macro_expansion_reference() {
364        let edge = EdgeKind::MacroExpansion {
365            expansion_kind: MacroExpansionKind::Derive,
366            is_verified: true,
367        };
368        let classified = EdgeClassification::from(&edge);
369        assert_eq!(classified, EdgeClassification::Reference);
370    }
371
372    #[test]
373    fn lifetime_constraint_reference() {
374        let edge = EdgeKind::LifetimeConstraint {
375            constraint_kind: LifetimeConstraintKind::Outlives,
376        };
377        let classified = EdgeClassification::from(&edge);
378        assert_eq!(classified, EdgeClassification::Reference);
379    }
380
381    #[test]
382    fn imports_wildcard() {
383        let edge = EdgeKind::Imports {
384            alias: None,
385            is_wildcard: true,
386        };
387        let classified = EdgeClassification::from(&edge);
388        assert_eq!(classified, EdgeClassification::Import { is_wildcard: true });
389    }
390
391    #[test]
392    fn inherits_classification() {
393        let edge = EdgeKind::Inherits;
394        let classified = EdgeClassification::from(&edge);
395        assert_eq!(classified, EdgeClassification::Inherits);
396    }
397
398    #[test]
399    fn implements_classification() {
400        let edge = EdgeKind::Implements;
401        let classified = EdgeClassification::from(&edge);
402        assert_eq!(classified, EdgeClassification::Implements);
403    }
404
405    #[test]
406    fn references_classification() {
407        let edge = EdgeKind::References;
408        let classified = EdgeClassification::from(&edge);
409        assert_eq!(classified, EdgeClassification::Reference);
410    }
411
412    #[test]
413    fn defines_classification() {
414        let edge = EdgeKind::Defines;
415        let classified = EdgeClassification::from(&edge);
416        assert_eq!(classified, EdgeClassification::Defines);
417    }
418
419    #[test]
420    fn contains_classification() {
421        let edge = EdgeKind::Contains;
422        let classified = EdgeClassification::from(&edge);
423        assert_eq!(classified, EdgeClassification::Contains);
424    }
425
426    #[test]
427    fn type_of_classification() {
428        let edge = EdgeKind::TypeOf {
429            context: None,
430            index: None,
431            name: None,
432        };
433        let classified = EdgeClassification::from(&edge);
434        assert_eq!(classified, EdgeClassification::TypeOf);
435    }
436
437    #[test]
438    fn message_queue_service_interaction() {
439        let edge = EdgeKind::MessageQueue {
440            protocol: crate::graph::unified::edge::kind::MqProtocol::Kafka,
441            topic: None,
442        };
443        let classified = EdgeClassification::from(&edge);
444        assert_eq!(classified, EdgeClassification::ServiceInteraction);
445    }
446
447    #[test]
448    fn websocket_service_interaction() {
449        let edge = EdgeKind::WebSocket { event: None };
450        let classified = EdgeClassification::from(&edge);
451        assert_eq!(classified, EdgeClassification::ServiceInteraction);
452    }
453
454    #[test]
455    fn grpc_call_cross_boundary() {
456        let edge = EdgeKind::GrpcCall {
457            service: test_string_id(),
458            method: test_string_id(),
459        };
460        let classified = EdgeClassification::from(&edge);
461        assert_eq!(
462            classified,
463            EdgeClassification::Call {
464                is_async: false,
465                is_cross_boundary: true,
466            }
467        );
468    }
469
470    #[test]
471    fn web_assembly_call_cross_boundary() {
472        let edge = EdgeKind::WebAssemblyCall;
473        let classified = EdgeClassification::from(&edge);
474        assert_eq!(
475            classified,
476            EdgeClassification::Call {
477                is_async: false,
478                is_cross_boundary: true,
479            }
480        );
481    }
482
483    #[test]
484    fn table_read_database_access() {
485        let edge = EdgeKind::TableRead {
486            table_name: test_string_id(),
487            schema: None,
488        };
489        let classified = EdgeClassification::from(&edge);
490        assert_eq!(classified, EdgeClassification::DatabaseAccess);
491    }
492
493    #[test]
494    fn table_write_database_access() {
495        let edge = EdgeKind::TableWrite {
496            table_name: test_string_id(),
497            schema: None,
498            operation: crate::graph::unified::edge::kind::TableWriteOp::Insert,
499        };
500        let classified = EdgeClassification::from(&edge);
501        assert_eq!(classified, EdgeClassification::DatabaseAccess);
502    }
503
504    #[test]
505    fn triggered_by_database_access() {
506        let edge = EdgeKind::TriggeredBy {
507            trigger_name: test_string_id(),
508            schema: None,
509        };
510        let classified = EdgeClassification::from(&edge);
511        assert_eq!(classified, EdgeClassification::DatabaseAccess);
512    }
513
514    #[test]
515    fn graphql_operation_service_interaction() {
516        let edge = EdgeKind::GraphQLOperation {
517            operation: test_string_id(),
518        };
519        let classified = EdgeClassification::from(&edge);
520        assert_eq!(classified, EdgeClassification::ServiceInteraction);
521    }
522
523    #[test]
524    fn process_exec_service_interaction() {
525        let edge = EdgeKind::ProcessExec {
526            command: test_string_id(),
527        };
528        let classified = EdgeClassification::from(&edge);
529        assert_eq!(classified, EdgeClassification::ServiceInteraction);
530    }
531
532    #[test]
533    fn file_ipc_service_interaction() {
534        let edge = EdgeKind::FileIpc { path_pattern: None };
535        let classified = EdgeClassification::from(&edge);
536        assert_eq!(classified, EdgeClassification::ServiceInteraction);
537    }
538
539    #[test]
540    fn protocol_call_service_interaction() {
541        let edge = EdgeKind::ProtocolCall {
542            protocol: test_string_id(),
543            metadata: None,
544        };
545        let classified = EdgeClassification::from(&edge);
546        assert_eq!(classified, EdgeClassification::ServiceInteraction);
547    }
548
549    #[test]
550    fn annotated_with_reference() {
551        let edge = EdgeKind::AnnotatedWith;
552        let classified = EdgeClassification::from(&edge);
553        assert_eq!(classified, EdgeClassification::Reference);
554    }
555
556    #[test]
557    fn annotation_param_reference() {
558        let edge = EdgeKind::AnnotationParam;
559        let classified = EdgeClassification::from(&edge);
560        assert_eq!(classified, EdgeClassification::Reference);
561    }
562
563    #[test]
564    fn lambda_captures_reference() {
565        let edge = EdgeKind::LambdaCaptures;
566        let classified = EdgeClassification::from(&edge);
567        assert_eq!(classified, EdgeClassification::Reference);
568    }
569
570    #[test]
571    fn extension_receiver_reference() {
572        let edge = EdgeKind::ExtensionReceiver;
573        let classified = EdgeClassification::from(&edge);
574        assert_eq!(classified, EdgeClassification::Reference);
575    }
576
577    #[test]
578    fn module_opens_export() {
579        let edge = EdgeKind::ModuleOpens;
580        let classified = EdgeClassification::from(&edge);
581        assert_eq!(
582            classified,
583            EdgeClassification::Export { is_reexport: false }
584        );
585    }
586
587    #[test]
588    fn module_requires_import() {
589        let edge = EdgeKind::ModuleRequires;
590        let classified = EdgeClassification::from(&edge);
591        assert_eq!(
592            classified,
593            EdgeClassification::Import { is_wildcard: false }
594        );
595    }
596
597    #[test]
598    fn module_provides_import() {
599        let edge = EdgeKind::ModuleProvides;
600        let classified = EdgeClassification::from(&edge);
601        assert_eq!(
602            classified,
603            EdgeClassification::Import { is_wildcard: false }
604        );
605    }
606
607    #[test]
608    fn type_argument_type_of() {
609        let edge = EdgeKind::TypeArgument;
610        let classified = EdgeClassification::from(&edge);
611        assert_eq!(classified, EdgeClassification::TypeOf);
612    }
613
614    #[test]
615    fn calls_sync_classification() {
616        let edge = EdgeKind::Calls {
617            argument_count: 0,
618            is_async: false,
619            resolved_via: ResolvedVia::Direct,
620        };
621        let classified = EdgeClassification::from(&edge);
622        assert_eq!(
623            classified,
624            EdgeClassification::Call {
625                is_async: false,
626                is_cross_boundary: false,
627            }
628        );
629    }
630}