Skip to main content

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}