sqry_core/graph/unified/edge/kind.rs
1//! `EdgeKind` enumeration for the unified graph architecture.
2//!
3//! This module defines `EdgeKind`, which categorizes all relationship types
4//! that can be represented as edges in the graph.
5//!
6//! # Design (, Appendix A2)
7//!
8//! The enumeration covers:
9//! - **Structural**: Defines, Contains
10//! - **References**: Calls, References, Imports, Exports, `TypeOf`
11//! - **OOP**: Inherits, Implements
12//! - **Cross-language**: FFI, HTTP, gRPC, WebAssembly, DB queries
13//! - **Extended**: `MessageQueue`, WebSocket, GraphQL, `ProcessExec`, `FileIpc`
14
15use std::fmt;
16
17use serde::{Deserialize, Serialize};
18use smallvec::SmallVec;
19
20use super::super::string::StringId;
21
22/// Resolution provenance for a `Calls` edge.
23///
24/// Discriminates how the call target was resolved during graph construction.
25/// Introduced by C-icall-precision Phase A (DESIGN §6); extended in Phase β
26/// joint-stubs (V12) with 5 additional dispatch-resolver provenances Plan B
27/// WS2 populates (`graph-fidelity-planner-correctness-dag.toml` line 221-239,
28/// DESIGN §3.2).
29///
30/// # Semantics
31///
32/// - `Direct` — the call target was resolved by the language plugin from a
33/// syntactic call expression (e.g., `f(x)` where `f` resolves to a single
34/// definition). This is the default and applies to every pre-Phase-A
35/// `Calls` edge (V10 wire compatibility).
36/// - `TypeMatch` — the call target was resolved post-hoc by flat type matching
37/// of indirect-call sites against compatible signatures. (Plan B DESIGN
38/// §3.2 names this `IndirectTypeMatch`; the V11 wire form names it
39/// `TypeMatch` and we keep that name for postcard on-disk stability.)
40/// - `BindingPlane` — the call target was resolved via the binding-plane
41/// designated-initializer mechanism (struct-field-of-function-pointer
42/// construction site witnesses). (Plan B DESIGN §3.2 names this
43/// `IndirectBindingPlane`; same naming-stability rationale as above.)
44/// - `VirtualDispatch` — JVM virtual / abstract method dispatch resolved
45/// through `Implements`/`Inherits` walks (Plan B `pass5c_jvm_virtual`).
46/// - `InterfaceDispatch` — Go interface dispatch resolved via structural
47/// method-set superset (Plan B `pass5d_go_interface`).
48/// - `DuckTyped` — Python duck-typed dispatch resolved by name+arity match
49/// on unknown-receiver call sites (Plan B `pass5e_python_duck`).
50/// - `Structural` — TypeScript structural dispatch resolved by declared
51/// interface superset (Plan B `pass5f_ts_structural`).
52/// - `PromiscuousElided` — fan-out cap exceeded (`CALLSITE_PROMISCUOUS`);
53/// resolver emitted a diagnostic self-edge instead of N targets.
54///
55/// # Wire compatibility (V11 → V12)
56///
57/// `ResolvedVia` is `#[repr(u16)]` with **explicit pinned discriminants**
58/// (0..=7) for V12 on-disk stability. Re-ordering or re-assigning these
59/// values is a snapshot-format breaking change — see Plan B DAG
60/// `critical_decisions` line 233 and DESIGN §3.2 line 239 ("Discriminants
61/// pinned: changing them later breaks V12 snapshots").
62///
63/// The serde `rename_all = "snake_case"` attribute governs JSON / human
64/// wire forms (planner text frontend, MCP filter params): the names emit
65/// as `direct`, `type_match`, `binding_plane`, `virtual_dispatch`,
66/// `interface_dispatch`, `duck_typed`, `structural`, `promiscuous_elided`.
67///
68/// Pre-Phase-A `Calls` payloads in **JSON** that omit the field
69/// deserialize with `ResolvedVia::Direct` (via `#[serde(default)]` on the
70/// `EdgeKind::Calls.resolved_via` field) — see test
71/// `calls_edge_json_default_old_wire` below.
72///
73/// **Postcard (the on-disk snapshot format) is positional and does NOT
74/// have a field-absence concept**, so `#[serde(default)]` cannot rescue a
75/// V10-shape postcard `Calls` payload (3 bytes: `[variant, argument_count,
76/// is_async]`). V10 → V11 postcard forward-compat is implemented in
77/// `sqry-core/src/graph/unified/persistence/snapshot.rs::upconvert_v10_to_v11`
78/// via explicit V10 type translation — that is the canonical V10 postcard
79/// reader path, not this serde annotation. V11 → V12 inherits this discipline:
80/// pre-V12 `ResolvedVia` payloads only carry variants 0..=2, and the V11
81/// upconvert preserves them unchanged.
82///
83/// # Why not an `EdgeKind::FfiCall` member
84///
85/// FFI calls are a distinct `EdgeKind` variant (`EdgeKind::FfiCall`) with
86/// their own metadata. `ResolvedVia` discriminates resolution strategy within
87/// the `Calls` variant, not edge-kind identity.
88#[repr(u16)]
89#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ResolvedVia {
92 /// Resolved directly by the language plugin from a syntactic call.
93 /// Pinned discriminant `0` for V12 on-disk stability.
94 #[default]
95 Direct = 0,
96 /// Resolved by flat type matching of an indirect call against compatible signatures.
97 /// Pinned discriminant `1` for V12 on-disk stability.
98 TypeMatch = 1,
99 /// Resolved via binding-plane designated-initializer witnesses.
100 /// Pinned discriminant `2` for V12 on-disk stability.
101 BindingPlane = 2,
102 /// JVM virtual / abstract method dispatch (Plan B `pass5c_jvm_virtual`).
103 /// Pinned discriminant `3` for V12 on-disk stability.
104 VirtualDispatch = 3,
105 /// Go interface dispatch (Plan B `pass5d_go_interface`).
106 /// Pinned discriminant `4` for V12 on-disk stability.
107 InterfaceDispatch = 4,
108 /// Python duck-typed dispatch (Plan B `pass5e_python_duck`).
109 /// Pinned discriminant `5` for V12 on-disk stability.
110 DuckTyped = 5,
111 /// TypeScript structural dispatch (Plan B `pass5f_ts_structural`).
112 /// Pinned discriminant `6` for V12 on-disk stability.
113 Structural = 6,
114 /// `CALLSITE_PROMISCUOUS` fan-out cap exceeded — resolver emitted a
115 /// diagnostic self-edge instead of N targets. Pinned discriminant `7`
116 /// for V12 on-disk stability.
117 PromiscuousElided = 7,
118}
119
120impl ResolvedVia {
121 /// Returns the pinned `u16` discriminant. Stable across V12 releases.
122 #[must_use]
123 pub const fn as_u16(self) -> u16 {
124 self as u16
125 }
126
127 /// All variants in pinned discriminant order. Convenience for tests
128 /// and downstream consumers that need to enumerate the resolution
129 /// provenance set.
130 pub const ALL: &'static [ResolvedVia] = &[
131 ResolvedVia::Direct,
132 ResolvedVia::TypeMatch,
133 ResolvedVia::BindingPlane,
134 ResolvedVia::VirtualDispatch,
135 ResolvedVia::InterfaceDispatch,
136 ResolvedVia::DuckTyped,
137 ResolvedVia::Structural,
138 ResolvedVia::PromiscuousElided,
139 ];
140}
141
142/// Context for `TypeOf` edges (parameter, return, field, variable).
143///
144/// Indicates where a type reference appears in the code structure.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum TypeOfContext {
148 /// Function/method parameter
149 Parameter,
150 /// Function/method return value
151 Return,
152 /// Struct/class field
153 Field,
154 /// Variable declaration
155 Variable,
156 /// Type parameter (generics)
157 TypeParameter,
158 /// Type constraint
159 Constraint,
160}
161
162/// FFI calling convention.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165#[derive(Default)]
166pub enum FfiConvention {
167 /// Standard C calling convention
168 #[default]
169 C,
170 /// cdecl calling convention
171 Cdecl,
172 /// stdcall calling convention (Windows)
173 Stdcall,
174 /// fastcall calling convention
175 Fastcall,
176 /// System default calling convention
177 System,
178}
179
180/// HTTP method for HTTP request edges.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "UPPERCASE")]
183#[derive(Default)]
184pub enum HttpMethod {
185 /// GET request
186 #[default]
187 Get,
188 /// POST request
189 Post,
190 /// PUT request
191 Put,
192 /// DELETE request
193 Delete,
194 /// PATCH request
195 Patch,
196 /// HEAD request
197 Head,
198 /// OPTIONS request
199 Options,
200 /// ALL methods (wildcard — matches any HTTP method)
201 All,
202}
203
204impl HttpMethod {
205 /// Returns the HTTP method as a string.
206 #[must_use]
207 pub const fn as_str(self) -> &'static str {
208 match self {
209 Self::Get => "GET",
210 Self::Post => "POST",
211 Self::Put => "PUT",
212 Self::Delete => "DELETE",
213 Self::Patch => "PATCH",
214 Self::Head => "HEAD",
215 Self::Options => "OPTIONS",
216 Self::All => "ALL",
217 }
218 }
219}
220
221/// Database query type for DB query edges.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224#[derive(Default)]
225pub enum DbQueryType {
226 /// SELECT query
227 #[default]
228 Select,
229 /// INSERT query
230 Insert,
231 /// UPDATE query
232 Update,
233 /// DELETE query
234 Delete,
235 /// EXECUTE stored procedure/function
236 Execute,
237}
238
239/// Database table write operation (SQL).
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum TableWriteOp {
243 /// INSERT operation
244 Insert,
245 /// UPDATE operation
246 Update,
247 /// DELETE operation
248 Delete,
249}
250
251/// Export kind for distinguishing re-exports from declarations.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254#[derive(Default)]
255pub enum ExportKind {
256 /// Direct export of a symbol
257 #[default]
258 Direct,
259 /// Re-export from another module
260 Reexport,
261 /// Default export (JavaScript/TypeScript)
262 Default,
263 /// Namespace export (export *)
264 Namespace,
265}
266
267/// Message queue protocol for async communication edges.
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case")]
270#[derive(Default)]
271pub enum MqProtocol {
272 /// Apache Kafka
273 #[default]
274 Kafka,
275 /// AWS SQS
276 Sqs,
277 /// `RabbitMQ` / AMQP
278 RabbitMq,
279 /// NATS messaging
280 Nats,
281 /// Redis Pub/Sub
282 Redis,
283 /// Other protocol (identified by `StringId`)
284 Other(StringId),
285}
286
287/// Kind of lifetime constraint relationship (Rust-specific).
288///
289/// Models the various ways lifetimes can constrain other lifetimes or types.
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
291#[serde(rename_all = "snake_case")]
292#[derive(Default)]
293pub enum LifetimeConstraintKind {
294 /// `'a: 'b` - lifetime 'a outlives 'b
295 #[default]
296 Outlives,
297 /// `T: 'a` - type T is bounded by lifetime 'a
298 TypeBound,
299 /// `&'a T` - reference with explicit lifetime
300 Reference,
301 /// `'static` bound
302 Static,
303 /// Higher-ranked trait bound: `for<'a> T: Trait<'a>`
304 HigherRanked,
305 /// Trait object bound: `dyn Trait + 'a`
306 TraitObject,
307 /// impl Trait bound: `impl Trait + 'a`
308 ImplTrait,
309 /// Elided lifetime (inferred by compiler, requires RA)
310 Elided,
311}
312
313/// Kind of macro expansion (Rust-specific).
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case")]
316#[derive(Default)]
317pub enum MacroExpansionKind {
318 /// Derive macro (`#[derive(...)]`)
319 Derive,
320 /// Attribute macro (`#[proc_macro_attribute]`)
321 Attribute,
322 /// Declarative macro (`macro_rules!`)
323 #[default]
324 Declarative,
325 /// Function-like macro
326 Function,
327 /// Conditional compilation gate (`#[cfg(...)]`, `#[cfg_attr(...)]`)
328 CfgGate,
329}
330
331/// Kind of error-wrapping relationship (T3 — Go error chains).
332///
333/// Distinguishes the seven source-syntax forms that produce a
334/// [`EdgeKind::Wraps`] edge. Each variant identifies the construct that
335/// authored the edge so downstream queries can filter by origin (e.g.
336/// "show me only `%w` format wraps" vs. "show me `Unwrap()` method wraps").
337///
338/// Variant ordering is significant for postcard serialization stability —
339/// add new variants at the end (after `ErrorsJoin`).
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
341#[serde(rename_all = "snake_case")]
342pub enum WrapKind {
343 /// `fmt.Errorf("...%w...", err)` — `%w` format verb wrapping.
344 #[default]
345 ErrorfVerb,
346 /// `func (e *E) Unwrap() error { return e.inner }` — single-error
347 /// `Unwrap` method.
348 UnwrapMethod,
349 /// `func (e *E) Unwrap() []error { return e.errs }` — multi-error
350 /// `Unwrap` method (Go 1.20+).
351 UnwrapMultiMethod,
352 /// `errors.Is(err, sentinel)` — sentinel-error comparison.
353 ErrorsIs,
354 /// `errors.As(err, &target)` — concrete-type extraction (target by reference).
355 ErrorsAs,
356 /// `errors.AsType[E](err)` — typed extraction (Go 1.26+).
357 ErrorsAsType,
358 /// `errors.Join(errs...)` — variadic joining (Go 1.20+).
359 ErrorsJoin,
360}
361
362/// Direction of a channel operation (Go T2.4).
363///
364/// Discriminates whether a [`EdgeKind::ChannelPeer`] edge records a send,
365/// a receive, or a close on the target [`super::super::node::kind::NodeKind::Channel`].
366/// Aligns with `GoGuard`'s producer / consumer abstraction (see
367/// `docs/development/go-channels-and-generic-instantiation/02_DESIGN.md` §1.3).
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub enum ChannelPeerDirection {
371 /// `ch <- v` send.
372 Send,
373 /// `<-ch` receive (expression, short-var, range, select receive arm).
374 Receive,
375 /// `close(ch)` builtin call.
376 Close,
377}
378
379/// Buffer classification of the channel an operation acts on (Go T2.4).
380///
381/// Cached on each [`EdgeKind::ChannelPeer`] edge from the owning `Channel`
382/// node so the planner can filter without joining through the node. The
383/// numeric capacity (for `Buffered`) lives on the `Channel` node metadata,
384/// not on the edge, to keep edge payloads compact across millions of edges.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum ChannelBufferKind {
388 /// `make(chan T)` — zero capacity.
389 Unbuffered,
390 /// `make(chan T, N)` with `N` resolved to a constant.
391 Buffered,
392 /// Capacity expression was non-constant, or the channel was reached
393 /// through a parameter / struct-field where the alias resolver did not
394 /// see the original `make` call.
395 Unknown,
396}
397
398/// How a generic instantiation's type-argument vector was derived (Go T2.5).
399///
400/// Carried on each [`EdgeKind::Instantiates`] edge. See
401/// `docs/development/go-channels-and-generic-instantiation/02_DESIGN.md` §3.2.
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
403#[serde(rename_all = "snake_case")]
404pub enum InferenceKind {
405 /// All type arguments were explicit (`Map[string, int](...)`).
406 Explicit,
407 /// All type arguments were inferred from function-argument types.
408 Inferred,
409 /// Explicit prefix + inferred / unknown suffix (the boldlygo.tech
410 /// "right-to-left omission" subset). `apply[[]int](nil, f)`.
411 Partial,
412 /// One or more slots were unsolvable by Phase 1 rules and recorded as
413 /// the `<unknown>` sentinel.
414 Unknown,
415}
416
417/// One slot in a generic instantiation's type-argument vector (Go T2.5).
418///
419/// `Copy` and 8 bytes (4-byte `StringId` + 1-byte bool + 3 padding) so a
420/// `SmallVec<[TypeArg; 4]>` inlines its common case on the stack.
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
422pub struct TypeArg {
423 /// Interned type-name string. The exact string `"<unknown>"` for
424 /// unresolved slots (no separate sentinel discriminant — see §4.4).
425 pub name: StringId,
426 /// True when the slot was filled by Go's untyped-constant default rule
427 /// (`int` for untyped int, `float64` for untyped float, etc. — AC-10).
428 /// Always `false` for the `<unknown>` sentinel.
429 pub default_typed: bool,
430}
431
432/// Enumeration of edge relationship types in the graph.
433///
434/// Each variant represents a distinct kind of relationship between nodes.
435/// The categorization is language-agnostic to support cross-language analysis.
436///
437/// Note: Uses default externally-tagged enum representation for serialization compatibility.
438/// JSON output will be `{"calls": {"argument_count": 0, "is_async": false}}` rather than
439/// `{"type": "calls", "argument_count": 0, "is_async": false}`.
440#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
441#[serde(rename_all = "snake_case")]
442pub enum EdgeKind {
443 // ==================== Structural ====================
444 /// A symbol defines another (e.g., module defines function).
445 Defines,
446
447 /// A container contains another (e.g., class contains method).
448 Contains,
449
450 // ==================== References ====================
451 /// A function/method calls another.
452 Calls {
453 /// Number of arguments in the call (0-255)
454 argument_count: u8,
455 /// Whether the call expression is directly awaited (uses `.await`).
456 ///
457 /// This indicates an *awaited call site*, not merely "inside an async function".
458 is_async: bool,
459 /// How this call edge's target was resolved (DESIGN §6).
460 ///
461 /// New in Phase A. `#[serde(default)]` keeps V10 **JSON / key-value**
462 /// wire payloads (which can omit this field) decodable with
463 /// `ResolvedVia::Direct`. Postcard is positional and cannot rescue a
464 /// trailing-field absence — V10 postcard `Calls` bytes are decoded by
465 /// the snapshot persistence layer's explicit V10 reader path
466 /// (`sqry-core/src/graph/unified/persistence/snapshot.rs::upconvert_v10_to_v11`,
467 /// added by U03), NOT by this serde annotation.
468 #[serde(default)]
469 resolved_via: ResolvedVia,
470 },
471
472 /// A symbol references another (read access).
473 References,
474
475 /// An import statement brings in a symbol.
476 Imports {
477 /// Optional alias for the import (e.g., `import { foo as bar }`)
478 alias: Option<StringId>,
479 /// Whether this is a wildcard import (e.g., `import *`)
480 is_wildcard: bool,
481 },
482
483 /// An export statement exposes a symbol.
484 Exports {
485 /// The kind of export (direct, re-export, default, namespace)
486 kind: ExportKind,
487 /// Optional alias for the export (e.g., `export { foo as bar }`)
488 alias: Option<StringId>,
489 },
490
491 /// A type reference with optional context metadata.
492 ///
493 /// Represents relationships like:
494 /// - Function parameter types (`context = Parameter`)
495 /// - Function return types (`context = Return`)
496 /// - Variable types (`context = Variable`)
497 /// - Struct field types (`context = Field`)
498 ///
499 /// The `context`, `index`, and `name` fields provide semantic information
500 /// about where and how the type reference appears.
501 TypeOf {
502 /// Context where this type reference appears
503 context: Option<TypeOfContext>,
504 /// Position/index (for parameters, returns, fields)
505 index: Option<u16>,
506 /// Name (for parameters, returns, fields, variables)
507 name: Option<StringId>,
508 },
509
510 // ==================== OOP ====================
511 /// A class inherits from another (extends).
512 Inherits,
513
514 /// A class/struct implements an interface/trait.
515 Implements,
516
517 // ==================== Rust-Specific ====================
518 /// Lifetime constraint relationship.
519 ///
520 /// Models Rust lifetime bounds. Source and target are `NodeKind::Lifetime`
521 /// or `NodeKind::Type` nodes (for type bounds like `T: 'a`).
522 ///
523 /// Query semantics: NOT included in `callers/callees` results.
524 /// Use dedicated query: `--lifetime-constraints`
525 LifetimeConstraint {
526 /// The kind of lifetime constraint
527 constraint_kind: LifetimeConstraintKind,
528 },
529
530 /// Trait method binding (call site -> impl method).
531 ///
532 /// Represents the resolution of a trait method call to a concrete
533 /// implementation. This is distinct from `Calls` because it involves
534 /// trait method resolution logic.
535 ///
536 /// Query semantics: NOT included in `callers` by default.
537 /// Use `--include-trait-bindings` flag or dedicated query.
538 TraitMethodBinding {
539 /// The trait providing the method
540 trait_name: StringId,
541 /// The implementing type
542 impl_type: StringId,
543 /// Whether this binding is ambiguous (multiple impls match)
544 is_ambiguous: bool,
545 },
546
547 /// Macro expansion relationship.
548 ///
549 /// Represents the expansion of a macro invocation to its generated code.
550 /// Only available when macro expansion is enabled (security opt-in).
551 ///
552 /// Query semantics: Included in `callees` when expansion enabled.
553 MacroExpansion {
554 /// The kind of macro expansion
555 expansion_kind: MacroExpansionKind,
556 /// Whether the expansion has been verified against source
557 is_verified: bool,
558 },
559
560 // ==================== Cross-language / Cross-service ====================
561 /// Foreign function interface call.
562 FfiCall {
563 /// The calling convention used
564 convention: FfiConvention,
565 },
566
567 /// HTTP request to an endpoint.
568 HttpRequest {
569 /// The HTTP method
570 method: HttpMethod,
571 /// Optional URL pattern
572 url: Option<StringId>,
573 },
574
575 /// gRPC service call.
576 GrpcCall {
577 /// Service name
578 service: StringId,
579 /// Method name
580 method: StringId,
581 },
582
583 /// WebAssembly function call.
584 WebAssemblyCall,
585
586 /// Database query execution.
587 DbQuery {
588 /// Query type
589 query_type: DbQueryType,
590 /// Optional table/collection name
591 table: Option<StringId>,
592 },
593
594 /// Database table read operation (SQL).
595 TableRead {
596 /// Name of the table being read
597 table_name: StringId,
598 /// Optional schema/database name
599 schema: Option<StringId>,
600 },
601
602 /// Database table write operation (SQL).
603 TableWrite {
604 /// Name of the table being written
605 table_name: StringId,
606 /// Optional schema/database name
607 schema: Option<StringId>,
608 /// Type of write operation (INSERT/UPDATE/DELETE)
609 operation: TableWriteOp,
610 },
611
612 /// Database trigger relationship (SQL).
613 TriggeredBy {
614 /// Name of the trigger
615 trigger_name: StringId,
616 /// Optional schema/database name
617 schema: Option<StringId>,
618 },
619
620 // ==================== Extended ====================
621 /// Message queue publish/subscribe.
622 MessageQueue {
623 /// Protocol used
624 protocol: MqProtocol,
625 /// Optional topic/queue name
626 topic: Option<StringId>,
627 },
628
629 /// WebSocket event communication.
630 WebSocket {
631 /// Optional event name
632 event: Option<StringId>,
633 },
634
635 /// GraphQL operation (query/mutation/subscription).
636 GraphQLOperation {
637 /// Operation name
638 operation: StringId,
639 },
640
641 /// Process execution (spawn, exec).
642 ProcessExec {
643 /// Command being executed
644 command: StringId,
645 },
646
647 /// File-based IPC (pipes, shared memory, temp files).
648 FileIpc {
649 /// Optional path pattern
650 path_pattern: Option<StringId>,
651 },
652
653 // ==================== Extensibility ====================
654 /// Generic protocol call for extensibility.
655 ProtocolCall {
656 /// Protocol identifier
657 protocol: StringId,
658 /// Optional JSON-encoded metadata
659 metadata: Option<StringId>,
660 },
661
662 // ==================== JVM Classpath (Track C) ====================
663 /// Generic type bound (e.g., `T extends Comparable<T>`).
664 GenericBound,
665
666 /// Symbol annotated with annotation type.
667 AnnotatedWith,
668
669 /// Annotation parameter binding (annotation -> element value).
670 AnnotationParam,
671
672 /// Lambda captures a method reference target.
673 LambdaCaptures,
674
675 /// Java module exports a package.
676 ModuleExports,
677
678 /// Java module requires another module.
679 ModuleRequires,
680
681 /// Java module opens a package for reflection.
682 ModuleOpens,
683
684 /// Java module provides a service implementation.
685 ModuleProvides,
686
687 /// Generic type argument (e.g., `String` in `List<String>`).
688 TypeArgument,
689
690 /// Kotlin extension function receiver type.
691 ExtensionReceiver,
692
693 /// Kotlin companion object relationship.
694 CompanionOf,
695
696 /// Kotlin sealed class permits a subclass.
697 SealedPermit,
698
699 // ==================== T3: Go error chains ====================
700 /// Error-wrapping relationship between a wrapper expression and a
701 /// wrapped error value.
702 ///
703 /// Emitted by Go plugin (T3.6) for `fmt.Errorf("%w", err)`, `Unwrap()`
704 /// method bodies, and the `errors.{Is,As,AsType,Join}` family. The
705 /// `kind` field identifies the source syntax; `chain_position` carries
706 /// the verb index for `%w` and the slice index for `errors.Join` /
707 /// `Unwrap() []error` slice literals (`None` for forms that do not
708 /// have a meaningful position).
709 ///
710 /// Query semantics: NOT included in `callers/callees` results.
711 /// Wrap-chain traversal lands later in T3 (planner `wraps:` predicate
712 /// in Cluster F; `relation_query`/dedicated tooling in Cluster G);
713 /// callers must walk `Wraps` edges explicitly until those surfaces
714 /// land. The MCP `context_propagation` tool (T3.7) is a separate
715 /// derived-cache query over span-resolved propagation chains, NOT
716 /// a wrap-edge traversal surface.
717 Wraps {
718 /// The source-syntax form that authored this edge.
719 kind: WrapKind,
720 /// Optional position within the wrap chain — verb index for
721 /// `%w` (0-based, skipping `%%`), slice index for `Unwrap()
722 /// []error` slice literals and `errors.Join` variadic args,
723 /// `None` for single-value forms.
724 chain_position: Option<u16>,
725 },
726
727 // ==================== T2.4: Go channel pairing ====================
728 /// Channel send / receive / close peer edge (Go T2.4).
729 ///
730 /// Edge **source**: a [`super::super::node::kind::NodeKind::CallSite`]
731 /// representing the operation site (the `ch <- v` send-statement node,
732 /// the `<-ch` unary-expr node, the `range ch` clause, the
733 /// `case ch <- v:` / `case <-ch:` select arm, or the `close(ch)`
734 /// builtin-call node).
735 ///
736 /// Edge **target**: a [`super::super::node::kind::NodeKind::Channel`]
737 /// representing the alias-class of the channel.
738 ///
739 /// Multiple edges per channel are expected — one per operation site.
740 /// `trace_path` walks send→channel←receive in two hops; consumers that
741 /// want a one-hop view filter by `direction` on both edges.
742 ///
743 /// Appended after the current terminal `Wraps` (T3 #279) so all existing
744 /// variant indices, including `Wraps`, are preserved on the postcard
745 /// wire. Rides the V13→V14 snapshot bump driven by the `NodeKind`
746 /// change (persistence §6.1).
747 ChannelPeer {
748 /// Whether this operation sends, receives, or closes.
749 direction: ChannelPeerDirection,
750 /// Cached classifier from the `Channel` node, replicated on the
751 /// edge so the planner can filter without joining through the
752 /// channel node.
753 buffer_kind: ChannelBufferKind,
754 },
755
756 // ==================== T2.5: Generic instantiation ====================
757 /// Generic-function call-site instantiation (Go T2.5; reusable for
758 /// Rust / TS / Java in later phases).
759 ///
760 /// Edge **source**: a [`super::super::node::kind::NodeKind::CallSite`]
761 /// for the generic call. Edge **target**: the generic function / method
762 /// definition.
763 ///
764 /// The edge **co-exists** with the existing `Calls` edge at the same
765 /// call site (AC-12 requires the `Calls` edge unchanged in every case);
766 /// the `Calls` edge carries `argument_count` and `is_async`, the
767 /// `Instantiates` edge carries the type-argument vector.
768 Instantiates {
769 /// Type arguments in declaration order. Each slot is a resolved
770 /// type name or the interned `"<unknown>"` sentinel.
771 /// `SmallVec<[TypeArg; 4]>` keeps the common 1-4-arg case on the
772 /// stack (most Go generics are 1-2 type parameters).
773 type_args: SmallVec<[TypeArg; 4]>,
774 /// Discriminator on how the type-arg vector was derived.
775 inference_kind: InferenceKind,
776 },
777}
778
779impl EdgeKind {
780 /// Returns `true` if this edge represents a function call relationship.
781 #[inline]
782 #[must_use]
783 pub const fn is_call(&self) -> bool {
784 matches!(
785 self,
786 Self::Calls { .. }
787 | Self::FfiCall { .. }
788 | Self::HttpRequest { .. }
789 | Self::GrpcCall { .. }
790 | Self::WebAssemblyCall
791 )
792 }
793
794 /// Returns `true` if this edge represents a structural relationship.
795 #[inline]
796 #[must_use]
797 pub const fn is_structural(&self) -> bool {
798 matches!(self, Self::Defines | Self::Contains)
799 }
800
801 /// Returns `true` if this edge represents a type relationship.
802 #[inline]
803 #[must_use]
804 pub const fn is_type_relation(&self) -> bool {
805 matches!(
806 self,
807 Self::Inherits
808 | Self::Implements
809 | Self::TypeOf { .. }
810 | Self::GenericBound
811 | Self::TypeArgument
812 | Self::ExtensionReceiver
813 | Self::SealedPermit
814 )
815 }
816
817 /// Returns `true` if this edge represents a cross-language/service boundary.
818 #[inline]
819 #[must_use]
820 pub const fn is_cross_boundary(&self) -> bool {
821 matches!(
822 self,
823 Self::FfiCall { .. }
824 | Self::HttpRequest { .. }
825 | Self::GrpcCall { .. }
826 | Self::WebAssemblyCall
827 | Self::DbQuery { .. }
828 | Self::TableRead { .. }
829 | Self::TableWrite { .. }
830 | Self::TriggeredBy { .. }
831 | Self::MessageQueue { .. }
832 | Self::WebSocket { .. }
833 | Self::GraphQLOperation { .. }
834 | Self::ProcessExec { .. }
835 | Self::FileIpc { .. }
836 | Self::ProtocolCall { .. }
837 )
838 }
839
840 /// Returns `true` if this is an async/message-based relationship.
841 #[inline]
842 #[must_use]
843 pub const fn is_async(&self) -> bool {
844 matches!(
845 self,
846 Self::MessageQueue { .. } | Self::WebSocket { .. } | Self::GraphQLOperation { .. }
847 )
848 }
849
850 /// Returns `true` if this is a Rust-specific edge kind.
851 ///
852 /// These edges are produced by the Rust language plugin and have
853 /// specialized query semantics.
854 #[inline]
855 #[must_use]
856 pub const fn is_rust_specific(&self) -> bool {
857 matches!(
858 self,
859 Self::LifetimeConstraint { .. }
860 | Self::TraitMethodBinding { .. }
861 | Self::MacroExpansion { .. }
862 )
863 }
864
865 /// Returns `true` if this is a lifetime constraint edge.
866 #[inline]
867 #[must_use]
868 pub const fn is_lifetime_constraint(&self) -> bool {
869 matches!(self, Self::LifetimeConstraint { .. })
870 }
871
872 /// Returns `true` if this is a trait method binding edge.
873 #[inline]
874 #[must_use]
875 pub const fn is_trait_method_binding(&self) -> bool {
876 matches!(self, Self::TraitMethodBinding { .. })
877 }
878
879 /// Returns `true` if this is a macro expansion edge.
880 #[inline]
881 #[must_use]
882 pub const fn is_macro_expansion(&self) -> bool {
883 matches!(self, Self::MacroExpansion { .. })
884 }
885
886 /// Returns the canonical tag name for this edge kind.
887 #[must_use]
888 pub const fn tag(&self) -> &'static str {
889 match self {
890 Self::Defines => "defines",
891 Self::Contains => "contains",
892 Self::Calls { .. } => "calls",
893 Self::References => "references",
894 Self::Imports { .. } => "imports",
895 Self::Exports { .. } => "exports",
896 Self::TypeOf { .. } => "type_of",
897 Self::Inherits => "inherits",
898 Self::Implements => "implements",
899 Self::LifetimeConstraint { .. } => "lifetime_constraint",
900 Self::TraitMethodBinding { .. } => "trait_method_binding",
901 Self::MacroExpansion { .. } => "macro_expansion",
902 Self::FfiCall { .. } => "ffi_call",
903 Self::HttpRequest { .. } => "http_request",
904 Self::GrpcCall { .. } => "grpc_call",
905 Self::WebAssemblyCall => "web_assembly_call",
906 Self::DbQuery { .. } => "db_query",
907 Self::TableRead { .. } => "table_read",
908 Self::TableWrite { .. } => "table_write",
909 Self::TriggeredBy { .. } => "triggered_by",
910 Self::MessageQueue { .. } => "message_queue",
911 Self::WebSocket { .. } => "web_socket",
912 Self::GraphQLOperation { .. } => "graphql_operation",
913 Self::ProcessExec { .. } => "process_exec",
914 Self::FileIpc { .. } => "file_ipc",
915 Self::ProtocolCall { .. } => "protocol_call",
916 Self::GenericBound => "generic_bound",
917 Self::AnnotatedWith => "annotated_with",
918 Self::AnnotationParam => "annotation_param",
919 Self::LambdaCaptures => "lambda_captures",
920 Self::ModuleExports => "module_exports",
921 Self::ModuleRequires => "module_requires",
922 Self::ModuleOpens => "module_opens",
923 Self::ModuleProvides => "module_provides",
924 Self::TypeArgument => "type_argument",
925 Self::ExtensionReceiver => "extension_receiver",
926 Self::CompanionOf => "companion_of",
927 Self::SealedPermit => "sealed_permit",
928 Self::Wraps { .. } => "wraps",
929 Self::ChannelPeer { .. } => "channel_peer",
930 Self::Instantiates { .. } => "instantiates",
931 }
932 }
933
934 /// Returns an estimated byte size for this edge kind variant.
935 ///
936 /// Used for byte-level admission control in the delta buffer.
937 /// Estimates are conservative approximations based on variant data.
938 ///
939 /// Not `const fn`: the `Instantiates` arm reads `type_args.len()`
940 /// (`SmallVec::len` is not const). The only caller
941 /// (`EdgeDelta::size`) is a runtime path.
942 #[must_use]
943 pub fn estimated_size(&self) -> usize {
944 // Base enum discriminant: 1 byte
945 // StringId: 4 bytes each
946 // Option<StringId>: 5 bytes (1 discriminant + 4 payload)
947 // bool: 1 byte, u8: 1 byte, ExportKind: 1 byte
948 match self {
949 // Unit variants: just discriminant
950 Self::Defines
951 | Self::Contains
952 | Self::References
953 | Self::Inherits
954 | Self::Implements
955 | Self::WebAssemblyCall
956 | Self::GenericBound
957 | Self::AnnotatedWith
958 | Self::AnnotationParam
959 | Self::LambdaCaptures
960 | Self::ModuleExports
961 | Self::ModuleRequires
962 | Self::ModuleOpens
963 | Self::ModuleProvides
964 | Self::TypeArgument
965 | Self::ExtensionReceiver
966 | Self::CompanionOf
967 | Self::SealedPermit => 1,
968
969 // u8 + bool: 1 + 1 + 1
970 // MacroExpansionKind + bool: 1 + 1
971 // ChannelPeerDirection + ChannelBufferKind: 1 + 1
972 Self::Calls { .. } | Self::MacroExpansion { .. } | Self::ChannelPeer { .. } => 3,
973
974 // Option<StringId> + bool: 5 + 1 + 1 (imports/exports)
975 // DbQueryType + Option<StringId>: 1 + 5
976 Self::Imports { .. } | Self::Exports { .. } | Self::DbQuery { .. } => 7,
977
978 // FfiConvention: 1 byte
979 // LifetimeConstraintKind: 1 byte
980 Self::FfiCall { .. } | Self::LifetimeConstraint { .. } => 2,
981
982 // StringId + StringId + bool: 4 + 4 + 1
983 // StringId + Option<StringId>: 4 + 5
984 Self::TraitMethodBinding { .. }
985 | Self::TableRead { .. }
986 | Self::TriggeredBy { .. }
987 | Self::ProtocolCall { .. } => 10,
988
989 // HttpMethod + Option<StringId>: 1 + 5
990 // Option<StringId>: 5 (websocket/file IPC)
991 Self::HttpRequest { .. } | Self::WebSocket { .. } | Self::FileIpc { .. } => 6,
992
993 // Two StringIds: 4 + 4
994 Self::GrpcCall { .. } => 9,
995
996 // StringId + Option<StringId> + TableWriteOp: 4 + 5 + 1
997 // Option<TypeOfContext> + Option<u16> + Option<StringId>: 2 + 3 + 5
998 Self::TableWrite { .. } | Self::MessageQueue { .. } | Self::TypeOf { .. } => 11,
999
1000 // StringId: 4
1001 Self::GraphQLOperation { .. } | Self::ProcessExec { .. } => 5,
1002
1003 // WrapKind: 1 + Option<u16>: 3 = 4
1004 Self::Wraps { .. } => 4,
1005
1006 // discriminant + len + N*(StringId + bool) + InferenceKind:
1007 // 1 + 4 + (len * 5) + 1
1008 Self::Instantiates { type_args, .. } => 6 + type_args.len() * 5,
1009 }
1010 }
1011}
1012
1013impl fmt::Display for EdgeKind {
1014 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1015 f.write_str(self.tag())
1016 }
1017}
1018
1019impl Default for EdgeKind {
1020 /// Returns `EdgeKind::Calls` as the default (most common edge type).
1021 fn default() -> Self {
1022 Self::Calls {
1023 argument_count: 0,
1024 is_async: false,
1025 resolved_via: ResolvedVia::Direct,
1026 }
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use super::*;
1033
1034 /// Helper to create a default Calls variant for tests.
1035 fn calls() -> EdgeKind {
1036 EdgeKind::Calls {
1037 argument_count: 0,
1038 is_async: false,
1039 resolved_via: ResolvedVia::Direct,
1040 }
1041 }
1042
1043 /// Helper to create a default Imports variant for tests.
1044 fn imports() -> EdgeKind {
1045 EdgeKind::Imports {
1046 alias: None,
1047 is_wildcard: false,
1048 }
1049 }
1050
1051 /// Helper to create a default Exports variant for tests.
1052 fn exports() -> EdgeKind {
1053 EdgeKind::Exports {
1054 kind: ExportKind::Direct,
1055 alias: None,
1056 }
1057 }
1058
1059 #[test]
1060 fn test_edge_kind_tag() {
1061 assert_eq!(calls().tag(), "calls");
1062 assert_eq!(imports().tag(), "imports");
1063 assert_eq!(exports().tag(), "exports");
1064 assert_eq!(EdgeKind::Defines.tag(), "defines");
1065 assert_eq!(
1066 EdgeKind::HttpRequest {
1067 method: HttpMethod::Get,
1068 url: None
1069 }
1070 .tag(),
1071 "http_request"
1072 );
1073 }
1074
1075 #[test]
1076 fn test_edge_kind_display() {
1077 assert_eq!(format!("{}", calls()), "calls");
1078 assert_eq!(format!("{}", imports()), "imports");
1079 assert_eq!(format!("{}", exports()), "exports");
1080 assert_eq!(format!("{}", EdgeKind::Inherits), "inherits");
1081 }
1082
1083 #[test]
1084 fn test_is_call() {
1085 assert!(calls().is_call());
1086 assert!(
1087 EdgeKind::Calls {
1088 argument_count: 5,
1089 is_async: true,
1090 resolved_via: ResolvedVia::Direct,
1091 }
1092 .is_call()
1093 );
1094 assert!(
1095 EdgeKind::FfiCall {
1096 convention: FfiConvention::C
1097 }
1098 .is_call()
1099 );
1100 assert!(
1101 EdgeKind::HttpRequest {
1102 method: HttpMethod::Post,
1103 url: None
1104 }
1105 .is_call()
1106 );
1107 assert!(!EdgeKind::Defines.is_call());
1108 assert!(!EdgeKind::Inherits.is_call());
1109 assert!(!imports().is_call());
1110 assert!(!exports().is_call());
1111 }
1112
1113 #[test]
1114 fn test_is_structural() {
1115 assert!(EdgeKind::Defines.is_structural());
1116 assert!(EdgeKind::Contains.is_structural());
1117 assert!(!calls().is_structural());
1118 assert!(!imports().is_structural());
1119 assert!(!exports().is_structural());
1120 }
1121
1122 #[test]
1123 fn test_is_type_relation() {
1124 assert!(EdgeKind::Inherits.is_type_relation());
1125 assert!(EdgeKind::Implements.is_type_relation());
1126 assert!(
1127 EdgeKind::TypeOf {
1128 context: None,
1129 index: None,
1130 name: None,
1131 }
1132 .is_type_relation()
1133 );
1134 assert!(!calls().is_type_relation());
1135 }
1136
1137 #[test]
1138 fn test_is_cross_boundary() {
1139 assert!(
1140 EdgeKind::FfiCall {
1141 convention: FfiConvention::C
1142 }
1143 .is_cross_boundary()
1144 );
1145 assert!(
1146 EdgeKind::HttpRequest {
1147 method: HttpMethod::Get,
1148 url: None
1149 }
1150 .is_cross_boundary()
1151 );
1152 assert!(
1153 EdgeKind::GrpcCall {
1154 service: StringId::INVALID,
1155 method: StringId::INVALID
1156 }
1157 .is_cross_boundary()
1158 );
1159 assert!(!calls().is_cross_boundary());
1160 assert!(!imports().is_cross_boundary());
1161 assert!(!exports().is_cross_boundary());
1162 }
1163
1164 #[test]
1165 fn test_is_async() {
1166 assert!(
1167 EdgeKind::MessageQueue {
1168 protocol: MqProtocol::Kafka,
1169 topic: None
1170 }
1171 .is_async()
1172 );
1173 assert!(EdgeKind::WebSocket { event: None }.is_async());
1174 assert!(!calls().is_async());
1175 // Note: EdgeKind::Calls with is_async: true still returns false from is_async()
1176 // because is_async() refers to async communication patterns, not async function calls
1177 assert!(
1178 !EdgeKind::Calls {
1179 argument_count: 0,
1180 is_async: true,
1181 resolved_via: ResolvedVia::Direct,
1182 }
1183 .is_async()
1184 );
1185 }
1186
1187 #[test]
1188 fn test_default() {
1189 assert_eq!(EdgeKind::default(), calls());
1190 assert_eq!(HttpMethod::default(), HttpMethod::Get);
1191 assert_eq!(FfiConvention::default(), FfiConvention::C);
1192 assert_eq!(DbQueryType::default(), DbQueryType::Select);
1193 assert_eq!(ExportKind::default(), ExportKind::Direct);
1194 }
1195
1196 #[test]
1197 fn test_http_method_as_str() {
1198 assert_eq!(HttpMethod::Get.as_str(), "GET");
1199 assert_eq!(HttpMethod::Post.as_str(), "POST");
1200 assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
1201 assert_eq!(HttpMethod::All.as_str(), "ALL");
1202 }
1203
1204 #[test]
1205 fn test_calls_with_metadata() {
1206 let sync_call = EdgeKind::Calls {
1207 argument_count: 3,
1208 is_async: false,
1209 resolved_via: ResolvedVia::Direct,
1210 };
1211 let async_call = EdgeKind::Calls {
1212 argument_count: 0,
1213 is_async: true,
1214 resolved_via: ResolvedVia::Direct,
1215 };
1216 assert_eq!(sync_call.tag(), "calls");
1217 assert_eq!(async_call.tag(), "calls");
1218 assert!(sync_call.is_call());
1219 assert!(async_call.is_call());
1220 assert_ne!(sync_call, async_call);
1221 }
1222
1223 #[test]
1224 fn test_imports_with_metadata() {
1225 let simple = imports();
1226 let aliased = EdgeKind::Imports {
1227 alias: Some(StringId::new(42)),
1228 is_wildcard: false,
1229 };
1230 let wildcard = EdgeKind::Imports {
1231 alias: None,
1232 is_wildcard: true,
1233 };
1234
1235 assert_eq!(simple.tag(), "imports");
1236 assert_eq!(aliased.tag(), "imports");
1237 assert_eq!(wildcard.tag(), "imports");
1238 assert_ne!(simple, aliased);
1239 assert_ne!(simple, wildcard);
1240 }
1241
1242 #[test]
1243 fn test_exports_with_metadata() {
1244 let direct = exports();
1245 let reexport = EdgeKind::Exports {
1246 kind: ExportKind::Reexport,
1247 alias: None,
1248 };
1249 let default_export = EdgeKind::Exports {
1250 kind: ExportKind::Default,
1251 alias: None,
1252 };
1253 let namespace = EdgeKind::Exports {
1254 kind: ExportKind::Namespace,
1255 alias: Some(StringId::new(1)),
1256 };
1257
1258 assert_eq!(direct.tag(), "exports");
1259 assert_eq!(reexport.tag(), "exports");
1260 assert_eq!(default_export.tag(), "exports");
1261 assert_eq!(namespace.tag(), "exports");
1262 assert_ne!(direct, reexport);
1263 assert_ne!(direct, default_export);
1264 }
1265
1266 #[test]
1267 fn test_serde_calls_imports_exports() {
1268 // Calls with metadata
1269 let calls = EdgeKind::Calls {
1270 argument_count: 5,
1271 is_async: true,
1272 resolved_via: ResolvedVia::Direct,
1273 };
1274 let json = serde_json::to_string(&calls).unwrap();
1275 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1276 assert_eq!(calls, deserialized);
1277 assert!(json.contains("\"calls\""));
1278 assert!(json.contains("\"argument_count\":5"));
1279 assert!(json.contains("\"is_async\":true"));
1280
1281 // Imports with alias
1282 let imports = EdgeKind::Imports {
1283 alias: Some(StringId::new(10)),
1284 is_wildcard: false,
1285 };
1286 let json = serde_json::to_string(&imports).unwrap();
1287 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1288 assert_eq!(imports, deserialized);
1289
1290 // Exports with kind
1291 let exports = EdgeKind::Exports {
1292 kind: ExportKind::Reexport,
1293 alias: None,
1294 };
1295 let json = serde_json::to_string(&exports).unwrap();
1296 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1297 assert_eq!(exports, deserialized);
1298 }
1299
1300 #[test]
1301 fn test_serde_complex_variants() {
1302 // HttpRequest with fields
1303 let http = EdgeKind::HttpRequest {
1304 method: HttpMethod::Post,
1305 url: None,
1306 };
1307 let json = serde_json::to_string(&http).unwrap();
1308 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1309 assert_eq!(http, deserialized);
1310
1311 // GrpcCall with StringIds
1312 let grpc = EdgeKind::GrpcCall {
1313 service: StringId::new(1),
1314 method: StringId::new(2),
1315 };
1316 let json = serde_json::to_string(&grpc).unwrap();
1317 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1318 assert_eq!(grpc, deserialized);
1319 }
1320
1321 #[test]
1322 fn test_postcard_roundtrip_simple_enums() {
1323 // Test postcard roundtrip for component enums used by EdgeKind.
1324
1325 // FfiConvention
1326 for conv in [
1327 FfiConvention::C,
1328 FfiConvention::Cdecl,
1329 FfiConvention::Stdcall,
1330 ] {
1331 let bytes = postcard::to_allocvec(&conv).unwrap();
1332 let deserialized: FfiConvention = postcard::from_bytes(&bytes).unwrap();
1333 assert_eq!(conv, deserialized);
1334 }
1335
1336 // HttpMethod
1337 for method in [
1338 HttpMethod::Get,
1339 HttpMethod::Post,
1340 HttpMethod::Delete,
1341 HttpMethod::All,
1342 ] {
1343 let bytes = postcard::to_allocvec(&method).unwrap();
1344 let deserialized: HttpMethod = postcard::from_bytes(&bytes).unwrap();
1345 assert_eq!(method, deserialized);
1346 }
1347
1348 // DbQueryType
1349 for query in [
1350 DbQueryType::Select,
1351 DbQueryType::Insert,
1352 DbQueryType::Update,
1353 ] {
1354 let bytes = postcard::to_allocvec(&query).unwrap();
1355 let deserialized: DbQueryType = postcard::from_bytes(&bytes).unwrap();
1356 assert_eq!(query, deserialized);
1357 }
1358
1359 // ExportKind
1360 for kind in [
1361 ExportKind::Direct,
1362 ExportKind::Reexport,
1363 ExportKind::Default,
1364 ExportKind::Namespace,
1365 ] {
1366 let bytes = postcard::to_allocvec(&kind).unwrap();
1367 let deserialized: ExportKind = postcard::from_bytes(&bytes).unwrap();
1368 assert_eq!(kind, deserialized);
1369 }
1370 }
1371
1372 #[test]
1373 fn test_edge_kind_json_compatibility() {
1374 // EdgeKind is designed for JSON serialization (MCP export).
1375 // Binary persistence in Phase 6 will use a custom format.
1376 let kinds = [
1377 calls(),
1378 imports(),
1379 exports(),
1380 EdgeKind::Defines,
1381 EdgeKind::HttpRequest {
1382 method: HttpMethod::Get,
1383 url: None,
1384 },
1385 EdgeKind::MessageQueue {
1386 protocol: MqProtocol::Kafka,
1387 topic: Some(StringId::new(1)),
1388 },
1389 ];
1390
1391 for kind in &kinds {
1392 // JSON roundtrip should work
1393 let json = serde_json::to_string(kind).unwrap();
1394 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1395 assert_eq!(*kind, deserialized);
1396
1397 // Postcard roundtrip should also work (required for graph persistence)
1398 let bytes = postcard::to_allocvec(kind).unwrap();
1399 let from_postcard: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1400 assert_eq!(*kind, from_postcard);
1401 }
1402 }
1403
1404 #[test]
1405 fn test_hash() {
1406 use std::collections::HashSet;
1407
1408 let mut set = HashSet::new();
1409 set.insert(calls());
1410 set.insert(imports());
1411 set.insert(exports());
1412 set.insert(EdgeKind::Defines);
1413 set.insert(EdgeKind::HttpRequest {
1414 method: HttpMethod::Get,
1415 url: None,
1416 });
1417
1418 assert!(set.contains(&calls()));
1419 assert!(set.contains(&imports()));
1420 assert!(set.contains(&exports()));
1421 assert!(!set.contains(&EdgeKind::Inherits));
1422 assert_eq!(set.len(), 5);
1423 }
1424
1425 #[test]
1426 fn test_ffi_convention_variants() {
1427 let conventions = [
1428 FfiConvention::C,
1429 FfiConvention::Cdecl,
1430 FfiConvention::Stdcall,
1431 FfiConvention::Fastcall,
1432 FfiConvention::System,
1433 ];
1434
1435 for conv in conventions {
1436 let edge = EdgeKind::FfiCall { convention: conv };
1437 assert!(edge.is_call());
1438 assert!(edge.is_cross_boundary());
1439 }
1440 }
1441
1442 #[test]
1443 fn test_mq_protocol_variants() {
1444 let protocols = [
1445 MqProtocol::Kafka,
1446 MqProtocol::Sqs,
1447 MqProtocol::RabbitMq,
1448 MqProtocol::Nats,
1449 MqProtocol::Redis,
1450 MqProtocol::Other(StringId::new(1)),
1451 ];
1452
1453 for proto in protocols {
1454 let edge = EdgeKind::MessageQueue {
1455 protocol: proto.clone(),
1456 topic: None,
1457 };
1458 assert!(edge.is_async());
1459 assert!(edge.is_cross_boundary());
1460 }
1461 }
1462
1463 #[test]
1464 fn test_export_kind_variants() {
1465 let kinds = [
1466 ExportKind::Direct,
1467 ExportKind::Reexport,
1468 ExportKind::Default,
1469 ExportKind::Namespace,
1470 ];
1471
1472 for kind in kinds {
1473 let edge = EdgeKind::Exports { kind, alias: None };
1474 assert_eq!(edge.tag(), "exports");
1475 assert!(!edge.is_call());
1476 assert!(!edge.is_structural());
1477 assert!(!edge.is_cross_boundary());
1478 }
1479 }
1480
1481 #[test]
1482 fn test_estimated_size() {
1483 // Unit variants
1484 assert_eq!(EdgeKind::Defines.estimated_size(), 1);
1485 assert_eq!(EdgeKind::Contains.estimated_size(), 1);
1486 assert_eq!(EdgeKind::References.estimated_size(), 1);
1487
1488 // Calls: u8 + bool = 3
1489 assert_eq!(calls().estimated_size(), 3);
1490
1491 // Imports: Option<StringId> + bool = 7
1492 assert_eq!(imports().estimated_size(), 7);
1493
1494 // Exports: ExportKind + Option<StringId> = 7
1495 assert_eq!(exports().estimated_size(), 7);
1496
1497 // Rust-specific edges
1498 assert_eq!(
1499 EdgeKind::LifetimeConstraint {
1500 constraint_kind: LifetimeConstraintKind::Outlives
1501 }
1502 .estimated_size(),
1503 2
1504 );
1505 assert_eq!(
1506 EdgeKind::MacroExpansion {
1507 expansion_kind: MacroExpansionKind::Derive,
1508 is_verified: true
1509 }
1510 .estimated_size(),
1511 3
1512 );
1513 assert_eq!(
1514 EdgeKind::TraitMethodBinding {
1515 trait_name: StringId::INVALID,
1516 impl_type: StringId::INVALID,
1517 is_ambiguous: false
1518 }
1519 .estimated_size(),
1520 10
1521 );
1522 }
1523
1524 // ==================== Rust-Specific Edge Tests ====================
1525
1526 #[test]
1527 fn test_lifetime_constraint_kind_variants() {
1528 let kinds = [
1529 LifetimeConstraintKind::Outlives,
1530 LifetimeConstraintKind::TypeBound,
1531 LifetimeConstraintKind::Reference,
1532 LifetimeConstraintKind::Static,
1533 LifetimeConstraintKind::HigherRanked,
1534 LifetimeConstraintKind::TraitObject,
1535 LifetimeConstraintKind::ImplTrait,
1536 LifetimeConstraintKind::Elided,
1537 ];
1538
1539 for constraint_kind in kinds {
1540 let edge = EdgeKind::LifetimeConstraint { constraint_kind };
1541 assert!(edge.is_rust_specific());
1542 assert!(edge.is_lifetime_constraint());
1543 assert!(!edge.is_call());
1544 assert!(!edge.is_structural());
1545 assert_eq!(edge.tag(), "lifetime_constraint");
1546 }
1547 }
1548
1549 #[test]
1550 fn test_macro_expansion_kind_variants() {
1551 let kinds = [
1552 MacroExpansionKind::Derive,
1553 MacroExpansionKind::Attribute,
1554 MacroExpansionKind::Declarative,
1555 MacroExpansionKind::Function,
1556 MacroExpansionKind::CfgGate,
1557 ];
1558
1559 for expansion_kind in kinds {
1560 let edge = EdgeKind::MacroExpansion {
1561 expansion_kind,
1562 is_verified: true,
1563 };
1564 assert!(edge.is_rust_specific());
1565 assert!(edge.is_macro_expansion());
1566 assert!(!edge.is_call());
1567 assert_eq!(edge.tag(), "macro_expansion");
1568 }
1569 }
1570
1571 #[test]
1572 fn test_trait_method_binding() {
1573 let edge = EdgeKind::TraitMethodBinding {
1574 trait_name: StringId::new(1),
1575 impl_type: StringId::new(2),
1576 is_ambiguous: false,
1577 };
1578
1579 assert!(edge.is_rust_specific());
1580 assert!(edge.is_trait_method_binding());
1581 assert!(!edge.is_call());
1582 assert_eq!(edge.tag(), "trait_method_binding");
1583
1584 // Test ambiguous binding
1585 let ambiguous = EdgeKind::TraitMethodBinding {
1586 trait_name: StringId::new(1),
1587 impl_type: StringId::new(2),
1588 is_ambiguous: true,
1589 };
1590 assert!(ambiguous.is_trait_method_binding());
1591 }
1592
1593 #[test]
1594 fn test_rust_specific_edges_serde() {
1595 // LifetimeConstraint
1596 let lifetime = EdgeKind::LifetimeConstraint {
1597 constraint_kind: LifetimeConstraintKind::HigherRanked,
1598 };
1599 let json = serde_json::to_string(&lifetime).unwrap();
1600 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1601 assert_eq!(lifetime, deserialized);
1602
1603 // TraitMethodBinding
1604 let binding = EdgeKind::TraitMethodBinding {
1605 trait_name: StringId::new(10),
1606 impl_type: StringId::new(20),
1607 is_ambiguous: true,
1608 };
1609 let json = serde_json::to_string(&binding).unwrap();
1610 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1611 assert_eq!(binding, deserialized);
1612
1613 // MacroExpansion
1614 let expansion = EdgeKind::MacroExpansion {
1615 expansion_kind: MacroExpansionKind::Derive,
1616 is_verified: false,
1617 };
1618 let json = serde_json::to_string(&expansion).unwrap();
1619 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1620 assert_eq!(expansion, deserialized);
1621 }
1622
1623 #[test]
1624 fn test_rust_specific_edges_postcard() {
1625 let edges = [
1626 EdgeKind::LifetimeConstraint {
1627 constraint_kind: LifetimeConstraintKind::Outlives,
1628 },
1629 EdgeKind::TraitMethodBinding {
1630 trait_name: StringId::new(5),
1631 impl_type: StringId::new(6),
1632 is_ambiguous: false,
1633 },
1634 EdgeKind::MacroExpansion {
1635 expansion_kind: MacroExpansionKind::Attribute,
1636 is_verified: true,
1637 },
1638 ];
1639
1640 for edge in edges {
1641 let bytes = postcard::to_allocvec(&edge).unwrap();
1642 let deserialized: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1643 assert_eq!(edge, deserialized);
1644 }
1645 }
1646
1647 #[test]
1648 fn test_lifetime_constraint_kind_defaults() {
1649 assert_eq!(
1650 LifetimeConstraintKind::default(),
1651 LifetimeConstraintKind::Outlives
1652 );
1653 assert_eq!(
1654 MacroExpansionKind::default(),
1655 MacroExpansionKind::Declarative
1656 );
1657 }
1658
1659 #[test]
1660 fn wraps_edge_serde_roundtrip() {
1661 // T3 Cluster A — exercises postcard + serde_json roundtrip across
1662 // every WrapKind, with and without chain_position. Variant ordering
1663 // is wire-format-significant (postcard encodes enum discriminants by
1664 // declaration index); this test pins the seven variants in order.
1665 let wrap_kinds = [
1666 WrapKind::ErrorfVerb,
1667 WrapKind::UnwrapMethod,
1668 WrapKind::UnwrapMultiMethod,
1669 WrapKind::ErrorsIs,
1670 WrapKind::ErrorsAs,
1671 WrapKind::ErrorsAsType,
1672 WrapKind::ErrorsJoin,
1673 ];
1674
1675 for kind in wrap_kinds {
1676 for chain_position in [None, Some(0u16), Some(7u16), Some(u16::MAX)] {
1677 let edge = EdgeKind::Wraps {
1678 kind,
1679 chain_position,
1680 };
1681
1682 let bytes = postcard::to_allocvec(&edge).unwrap();
1683 let postcard_back: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1684 assert_eq!(
1685 edge, postcard_back,
1686 "postcard roundtrip mismatch for kind={kind:?} chain_position={chain_position:?}"
1687 );
1688
1689 let json = serde_json::to_string(&edge).unwrap();
1690 let json_back: EdgeKind = serde_json::from_str(&json).unwrap();
1691 assert_eq!(
1692 edge, json_back,
1693 "serde_json roundtrip mismatch for kind={kind:?} chain_position={chain_position:?}"
1694 );
1695 assert!(
1696 json.contains("\"wraps\""),
1697 "JSON encoding must use snake_case tag `wraps`: {json}"
1698 );
1699 // Pin the inner `WrapKind` snake_case spelling so
1700 // removing `#[serde(rename_all = "snake_case")]` from
1701 // WrapKind breaks this test. Without these asserts the
1702 // roundtrip alone would silently accept PascalCase
1703 // (deserialize accepts what serialize emits).
1704 let expected_kind_str = match kind {
1705 WrapKind::ErrorfVerb => "errorf_verb",
1706 WrapKind::UnwrapMethod => "unwrap_method",
1707 WrapKind::UnwrapMultiMethod => "unwrap_multi_method",
1708 WrapKind::ErrorsIs => "errors_is",
1709 WrapKind::ErrorsAs => "errors_as",
1710 WrapKind::ErrorsAsType => "errors_as_type",
1711 WrapKind::ErrorsJoin => "errors_join",
1712 };
1713 assert!(
1714 json.contains(&format!("\"{expected_kind_str}\"")),
1715 "JSON encoding must carry snake_case WrapKind `{expected_kind_str}`: {json}"
1716 );
1717 }
1718 }
1719
1720 // Default WrapKind must be the first variant (ErrorfVerb) — the
1721 // serialization-stability comment on WrapKind requires append-only
1722 // variant ordering, and `#[default]` is on ErrorfVerb.
1723 assert_eq!(WrapKind::default(), WrapKind::ErrorfVerb);
1724
1725 // Tag is stable across all variants and chain positions.
1726 assert_eq!(
1727 EdgeKind::Wraps {
1728 kind: WrapKind::ErrorfVerb,
1729 chain_position: Some(0),
1730 }
1731 .tag(),
1732 "wraps"
1733 );
1734 }
1735
1736 // ========================================================================
1737 // ResolvedVia tests (TEST:c-icall-precision-017)
1738 //
1739 // Cover the four acceptance criteria for U04_RESOLVED_VIA:
1740 // 1. ResolvedVia::default() == ResolvedVia::Direct
1741 // 2. serde rename_all = "snake_case" produces `direct` / `type_match` /
1742 // `binding_plane` and round-trips for all three variants
1743 // 3. `#[serde(default)]` on Calls.resolved_via lets a V10-shape Calls
1744 // payload (without `resolved_via` field) decode into V11 shape with
1745 // `resolved_via == Direct` — covered for **JSON / key-value** formats
1746 // only. Postcard old-wire forward-compat lives in
1747 // `sqry-core/src/graph/unified/persistence/snapshot.rs::upconvert_v10_to_v11`
1748 // (U03's explicit V10 reader path), NOT in this serde annotation.
1749 // 4. Full Calls round-trip preserves `resolved_via` non-default values
1750 // end-to-end via JSON and postcard
1751 // ========================================================================
1752
1753 /// `ResolvedVia::default()` must return `Direct` so pre-Phase-A `Calls`
1754 /// edges retain their semantic provenance without explicit construction.
1755 /// See DESIGN §6.1 and U04 critical decisions in the DAG.
1756 #[test]
1757 fn calls_edge_resolved_via_default_is_direct() {
1758 assert_eq!(ResolvedVia::default(), ResolvedVia::Direct);
1759
1760 // An `EdgeKind::Calls` value constructed without an explicit
1761 // `resolved_via` (via field-default) also yields `Direct`.
1762 let kind = EdgeKind::Calls {
1763 argument_count: 0,
1764 is_async: false,
1765 resolved_via: ResolvedVia::default(),
1766 };
1767 if let EdgeKind::Calls { resolved_via, .. } = kind {
1768 assert_eq!(resolved_via, ResolvedVia::Direct);
1769 } else {
1770 unreachable!("EdgeKind::Calls construction must be reachable");
1771 }
1772 }
1773
1774 /// `#[serde(rename_all = "snake_case")]` must produce the three exact wire
1775 /// spellings the planner predicate parser depends on
1776 /// (`direct` / `type_match` / `binding_plane` — DESIGN §11.2).
1777 #[test]
1778 fn calls_edge_resolved_via_serde_snake_case_round_trip() {
1779 for (variant, wire) in [
1780 (ResolvedVia::Direct, "\"direct\""),
1781 (ResolvedVia::TypeMatch, "\"type_match\""),
1782 (ResolvedVia::BindingPlane, "\"binding_plane\""),
1783 ] {
1784 let json = serde_json::to_string(&variant).unwrap();
1785 assert_eq!(json, wire, "ResolvedVia::{variant:?} serializes to {wire}");
1786 let parsed: ResolvedVia = serde_json::from_str(&json).unwrap();
1787 assert_eq!(parsed, variant, "round-trip for {wire}");
1788 }
1789 }
1790
1791 /// `#[serde(default)]` on `EdgeKind::Calls.resolved_via` lets a V10-shape
1792 /// Calls payload (no `resolved_via` field) decode into the V11 shape with
1793 /// `resolved_via = ResolvedVia::Direct` **for key-value formats like JSON
1794 /// where field absence is expressible**. Postcard (the on-disk snapshot
1795 /// format) is a positional binary format with no "field absence" concept,
1796 /// so `#[serde(default)]` cannot rescue a V10-shape postcard payload —
1797 /// V10 postcard bytes for `Calls` end after `is_async` and decoding as
1798 /// V11 fails with "Hit the end of buffer, expected more data".
1799 ///
1800 /// V10 → V11 postcard forward-compat lives in the snapshot persistence
1801 /// layer (`sqry-core/src/graph/unified/persistence/snapshot.rs`,
1802 /// `upconvert_v10_to_v11`) via explicit V10 type translation, NOT via
1803 /// `#[serde(default)]` on this enum. This test is therefore scoped to
1804 /// the JSON path only — the formal V11 wire round-trip lives in U06.
1805 #[test]
1806 fn calls_edge_json_default_old_wire() {
1807 // Hand-craft a V10-shape Calls payload via a parallel struct that
1808 // serializes to the same wire format as pre-Phase-A `Calls` did.
1809 #[derive(Serialize)]
1810 #[serde(rename_all = "snake_case")]
1811 enum LegacyEdgeKind {
1812 #[serde(rename = "calls")]
1813 Calls { argument_count: u8, is_async: bool },
1814 }
1815
1816 let legacy = LegacyEdgeKind::Calls {
1817 argument_count: 7,
1818 is_async: true,
1819 };
1820
1821 // ---- JSON path ----
1822 let legacy_json = serde_json::to_string(&legacy).unwrap();
1823 // Sanity-check: the legacy payload literally omits `resolved_via`.
1824 assert!(!legacy_json.contains("resolved_via"));
1825
1826 let decoded: EdgeKind = serde_json::from_str(&legacy_json).unwrap();
1827 match decoded {
1828 EdgeKind::Calls {
1829 argument_count,
1830 is_async,
1831 resolved_via,
1832 } => {
1833 assert_eq!(argument_count, 7);
1834 assert!(is_async);
1835 assert_eq!(resolved_via, ResolvedVia::Direct);
1836 }
1837 other => panic!("expected EdgeKind::Calls, got {other:?}"),
1838 }
1839 }
1840
1841 /// Two `Calls` edges that differ only in `resolved_via` must be unequal
1842 /// — that's the planner's semantic-discriminator contract (DESIGN §6.3bis
1843 /// Mechanism A). Combined with full JSON + postcard round-trip, this also
1844 /// confirms `TypeMatch` and `BindingPlane` survive every wire path.
1845 #[test]
1846 fn calls_edge_resolved_via_distinguishes_variants_round_trip() {
1847 let direct = EdgeKind::Calls {
1848 argument_count: 2,
1849 is_async: true,
1850 resolved_via: ResolvedVia::Direct,
1851 };
1852 let type_match = EdgeKind::Calls {
1853 argument_count: 2,
1854 is_async: true,
1855 resolved_via: ResolvedVia::TypeMatch,
1856 };
1857 let binding_plane = EdgeKind::Calls {
1858 argument_count: 2,
1859 is_async: true,
1860 resolved_via: ResolvedVia::BindingPlane,
1861 };
1862
1863 // Field-level discrimination — required so the planner's edge-kind
1864 // discriminator can fuse / dedup correctly per DESIGN §6.3bis.
1865 assert_ne!(direct, type_match);
1866 assert_ne!(direct, binding_plane);
1867 assert_ne!(type_match, binding_plane);
1868 // Same kind tag despite distinct `resolved_via` values.
1869 assert_eq!(direct.tag(), "calls");
1870 assert_eq!(type_match.tag(), "calls");
1871 assert_eq!(binding_plane.tag(), "calls");
1872
1873 // JSON round-trip preserves every field including `resolved_via`.
1874 for edge in [&direct, &type_match, &binding_plane] {
1875 let json = serde_json::to_string(edge).unwrap();
1876 assert!(
1877 json.contains("\"resolved_via\":"),
1878 "non-default Calls must emit `resolved_via` on the wire: {json}"
1879 );
1880 let decoded: EdgeKind = serde_json::from_str(&json).unwrap();
1881 assert_eq!(&decoded, edge);
1882 }
1883
1884 // Postcard round-trip is the on-disk graph-snapshot format
1885 // (V10+ uses postcard for graph payloads — see persistence::snapshot).
1886 for edge in [&direct, &type_match, &binding_plane] {
1887 let bytes = postcard::to_allocvec(edge).unwrap();
1888 let decoded: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1889 assert_eq!(&decoded, edge);
1890 }
1891 }
1892}