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