Skip to main content

sqry_core/graph/unified/build/
helper.rs

1//! Helper utilities for `GraphBuilder` implementations.
2//!
3//! This module provides high-level abstractions that make it easier to
4//! implement `GraphBuilder::build_graph()` using the `StagingGraph` API.
5//!
6//! # Overview
7//!
8//! The [`GraphBuildHelper`] wraps a `&mut StagingGraph` and provides:
9//! - Local string interning with `StringId` tracking
10//! - Qualified name to `NodeId` mapping
11//! - High-level node creation methods
12//! - High-level edge creation methods
13#![allow(clippy::similar_names)] // Domain terminology uses caller/callee and importer/imported pairs.
14//!
15//! # Usage
16//!
17//! ```ignore
18//! fn build_graph(
19//!     &self,
20//!     tree: &Tree,
21//!     content: &[u8],
22//!     file: &Path,
23//!     staging: &mut StagingGraph,
24//! ) -> GraphResult<()> {
25//!     let mut helper = GraphBuildHelper::new(staging, file, Language::Rust);
26//!
27//!     // Create function nodes
28//!     let main_id = helper.add_function("main", None, false, false)?;
29//!     let helper_id = helper.add_function("helper", None, false, false)?;
30//!
31//!     // Create call edge
32//!     helper.add_call_edge(main_id, helper_id);
33//!
34//!     Ok(())
35//! }
36//! ```
37//!
38//! This helper provides a high-level API that mirrors the patterns plugins use
39//! with `StagingGraph`, reducing boilerplate in `GraphBuilder` implementations.
40
41use std::collections::HashMap;
42use std::path::Path;
43
44use super::super::edge::kind::{LifetimeConstraintKind, MacroExpansionKind, TypeOfContext};
45use super::super::resolution::canonicalize_graph_qualified_name;
46use super::staging::{
47    CIndirectStagingPayload, NodeMetadataFlag, NodeMetadataUpdate, PendingBinding,
48    PendingIndirectCallsite, StagingGraph,
49};
50use crate::graph::node::{Language, Span};
51use crate::graph::unified::edge::kind::{
52    ChannelBufferKind, ChannelPeerDirection, InferenceKind, TypeArg,
53};
54use crate::graph::unified::edge::{
55    EdgeKind, ExportKind, FfiConvention, HttpMethod, ResolvedVia, TableWriteOp, WrapKind,
56};
57use crate::graph::unified::file::FileId;
58use crate::graph::unified::node::{NodeId, NodeKind};
59use crate::graph::unified::storage::NodeEntry;
60use crate::graph::unified::storage::c_indirect::{BindingSiteKind, IndirectShape, LocalScopeIndex};
61use crate::graph::unified::string::StringId;
62
63/// Node kinds that represent callable targets and may be used interchangeably
64/// across files. When a plugin calls `ensure_function` for a name that already
65/// exists as any of these kinds, the existing node is reused instead of creating
66/// a duplicate spanless stub.
67///
68/// dec44131f established this for the Method<->Function pair. This const
69/// generalizes it to all call-compatible kinds.
70pub(crate) const CALL_COMPATIBLE_KINDS: &[NodeKind] = &[
71    NodeKind::Function,
72    NodeKind::Method,
73    NodeKind::Macro,
74    NodeKind::Constant,
75    NodeKind::LambdaTarget,
76];
77
78/// Hint for the kind of callee node to create when no cached node exists.
79///
80/// Only call-compatible kinds are valid hints. Using a non-call-compatible
81/// kind (e.g., `StyleRule`) is prevented at compile time by this enum.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum CalleeKindHint {
84    /// Default: create a `Function` node.
85    Function,
86    /// Create a `Method` node (receiver method).
87    Method,
88    /// Create a `Macro` node (C preprocessor macro, Rust macro, etc.).
89    Macro,
90    /// Create a `Constant` node (function pointer constant).
91    Constant,
92    /// Create a `LambdaTarget` node (Java SAM interface, Kotlin lambda, etc.).
93    LambdaTarget,
94    /// No preference: create a `Function` node (same as `Function`).
95    Any,
96}
97
98impl CalleeKindHint {
99    /// Convert to the default `NodeKind` for node creation.
100    fn to_node_kind(self) -> NodeKind {
101        match self {
102            Self::Function | Self::Any => NodeKind::Function,
103            Self::Method => NodeKind::Method,
104            Self::Macro => NodeKind::Macro,
105            Self::Constant => NodeKind::Constant,
106            Self::LambdaTarget => NodeKind::LambdaTarget,
107        }
108    }
109}
110
111/// Helper for building graphs in `GraphBuilder` implementations.
112///
113/// Provides high-level abstractions over `StagingGraph` that handle:
114/// - String interning with local ID tracking
115/// - Qualified name deduplication
116/// - Node and edge creation with proper types
117#[derive(Debug)]
118pub struct GraphBuildHelper<'a> {
119    /// The underlying staging graph.
120    staging: &'a mut StagingGraph,
121    /// Language for this file.
122    language: Language,
123    /// File ID (pre-allocated).
124    file_id: FileId,
125    /// File path for error messages.
126    file_path: String,
127    /// Local string interning: string value -> local `StringId`.
128    string_cache: HashMap<String, StringId>,
129    /// Next local string ID to allocate.
130    next_string_id: u32,
131    /// Qualified name -> `NodeId` mapping for deduplication.
132    ///
133    /// Shared by both canonical nodes (via `add_node_internal`, which stores
134    /// under the **canonicalized** qualified name) and verbatim nodes (via
135    /// `add_node_verbatim`, which stores under the **raw** name).  Collisions
136    /// are avoided because canonical names never contain native delimiters
137    /// (e.g. `.`, `#`) while verbatim names preserve them (e.g. `styles.css`).
138    node_cache: HashMap<(String, NodeKind), NodeId>,
139}
140
141impl<'a> GraphBuildHelper<'a> {
142    /// Create a new helper for the given staging graph and file.
143    ///
144    /// The `file_id` should be pre-allocated by the caller (typically 0 for
145    /// per-file staging buffers).
146    pub fn new(staging: &'a mut StagingGraph, file: &Path, language: Language) -> Self {
147        Self {
148            staging,
149            language,
150            file_id: FileId::new(0), // Per-file staging uses local file ID 0
151            file_path: file.display().to_string(),
152            string_cache: HashMap::new(),
153            next_string_id: 0,
154            node_cache: HashMap::new(),
155        }
156    }
157
158    /// Create a helper with a specific file ID.
159    pub fn with_file_id(
160        staging: &'a mut StagingGraph,
161        file: &Path,
162        language: Language,
163        file_id: FileId,
164    ) -> Self {
165        Self {
166            staging,
167            language,
168            file_id,
169            file_path: file.display().to_string(),
170            string_cache: HashMap::new(),
171            next_string_id: 0,
172            node_cache: HashMap::new(),
173        }
174    }
175
176    /// Get the language for this helper.
177    #[must_use]
178    pub fn language(&self) -> Language {
179        self.language
180    }
181
182    /// Get the file ID for this helper.
183    #[must_use]
184    pub fn file_id(&self) -> FileId {
185        self.file_id
186    }
187
188    /// Look up a node ID by its qualified name and kind from the internal cache.
189    ///
190    /// Returns the `NodeId` if a node with the given `(name, kind)` pair was
191    /// previously created through this helper. This is used by macro boundary
192    /// analysis to find graph nodes corresponding to AST items.
193    #[must_use]
194    pub fn lookup_node(&self, name: &str, kind: NodeKind) -> Option<NodeId> {
195        self.node_cache.get(&(name.to_string(), kind)).copied()
196    }
197
198    /// Get the file path.
199    #[must_use]
200    pub fn file_path(&self) -> &str {
201        &self.file_path
202    }
203
204    /// Mutable access to the underlying [`StagingGraph`].
205    ///
206    /// Exposed for plugin call sites that need to forward typed
207    /// metadata into the staging buffer alongside their normal `add_*`
208    /// node-creation flow — for example, the Go plugin's
209    /// `add_synthetic_variable` helper (`C_SUPPRESS`) which calls
210    /// [`StagingGraph::merge_macro_metadata`] to record a
211    /// `NodeFlags::SYNTHETIC` flag on the freshly-staged Variable
212    /// node so the suppression contract on
213    /// [`crate::graph::unified::concurrent::graph::GraphSnapshot::find_by_pattern`]
214    /// is satisfied via the canonical metadata-bit channel (in addition
215    /// to the structural name-shape fallback).
216    #[must_use]
217    pub fn staging_mut(&mut self) -> &mut StagingGraph {
218        self.staging
219    }
220
221    /// Attach body hashes to all staged nodes using the given content bytes.
222    ///
223    /// Multi-language plugins (Vue, Svelte) should call this per extracted
224    /// script block so that node body spans — which are relative to the
225    /// block content, not the full SFC file — produce correct hashes.
226    /// Nodes that already have a hash are skipped, so the later whole-file
227    /// call in the indexing entrypoint is harmless.
228    pub fn attach_body_hashes(&mut self, content: &[u8]) {
229        // Per-block helper (Vue/Svelte embedded scripts): body hashes only. Shape
230        // descriptors for embedded-script blocks are a later concern; the whole-file
231        // index seam in the entrypoint owns shape wiring.
232        self.staging.attach_body_hashes(content, None);
233    }
234
235    /// Intern a string and get a local `StringId`.
236    ///
237    /// Strings are deduplicated: calling with the same value returns the same ID.
238    /// The local `StringId` is passed to the staging graph so that during
239    /// `commit_strings()`, a remap table from local to global IDs can be built.
240    pub fn intern(&mut self, s: &str) -> StringId {
241        if let Some(&id) = self.string_cache.get(s) {
242            return id;
243        }
244
245        let id = StringId::new_local(self.next_string_id);
246        self.next_string_id += 1;
247        self.string_cache.insert(s.to_string(), id);
248        // Pass the local_id to staging so it can build the remap table during commit
249        self.staging.intern_string(id, s.to_string());
250        id
251    }
252
253    /// Check if a node with the given qualified name already exists.
254    #[must_use]
255    pub fn has_node(&self, qualified_name: &str) -> bool {
256        self.node_cache
257            .keys()
258            .any(|(name, _)| name == qualified_name)
259    }
260
261    /// Get an existing node by qualified name.
262    #[must_use]
263    pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
264        self.node_cache
265            .iter()
266            .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
267    }
268
269    /// Check if a node with the given qualified name and kind already exists.
270    #[must_use]
271    pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
272        self.node_cache
273            .contains_key(&(qualified_name.to_string(), kind))
274    }
275
276    /// Get an existing node by qualified name and kind.
277    #[must_use]
278    pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
279        self.node_cache
280            .get(&(qualified_name.to_string(), kind))
281            .copied()
282    }
283
284    /// Add a function node with the given qualified name.
285    ///
286    /// Returns the `NodeId` (creating the node if it doesn't exist).
287    pub fn add_function(
288        &mut self,
289        qualified_name: &str,
290        span: Option<Span>,
291        is_async: bool,
292        is_unsafe: bool,
293    ) -> NodeId {
294        // Dual-use bare helper (issue #394): also used to create call/FFI/callee
295        // stubs (e.g. go syscall/ffi targets, rust trait-binding callees), so it
296        // defaults is_definition = false. Real declaration sites opt in via
297        // mark_definition (or use the _with_visibility/_with_signature variants).
298        self.add_function_inner(qualified_name, span, is_async, is_unsafe, false)
299    }
300
301    /// Internal function-node sink shared by the public declaration helper
302    /// [`add_function`](Self::add_function) (`is_definition = true`) and the
303    /// call-edge wrapper [`ensure_function`](Self::ensure_function)
304    /// (`is_definition = false`). The seam keeps real declarations marked as
305    /// definitions while call-target stubs created via the `ensure_*` path stay
306    /// non-definitions.
307    fn add_function_inner(
308        &mut self,
309        qualified_name: &str,
310        span: Option<Span>,
311        is_async: bool,
312        is_unsafe: bool,
313        is_definition: bool,
314    ) -> NodeId {
315        self.add_node_internal(
316            qualified_name,
317            span,
318            NodeKind::Function,
319            &[("async", is_async), ("unsafe", is_unsafe)],
320            None,
321            None,
322            is_definition,
323        )
324    }
325
326    /// Add a function node with visibility.
327    ///
328    /// Returns the `NodeId` (creating the node if it doesn't exist).
329    pub fn add_function_with_visibility(
330        &mut self,
331        qualified_name: &str,
332        span: Option<Span>,
333        is_async: bool,
334        is_unsafe: bool,
335        visibility: Option<&str>,
336    ) -> NodeId {
337        self.add_node_internal(
338            qualified_name,
339            span,
340            NodeKind::Function,
341            &[("async", is_async), ("unsafe", is_unsafe)],
342            visibility,
343            None,
344            true,
345        )
346    }
347
348    /// Add a function node with signature (return type).
349    ///
350    /// The signature is used for `returns:` queries.
351    /// Returns the `NodeId` (creating the node if it doesn't exist).
352    pub fn add_function_with_signature(
353        &mut self,
354        qualified_name: &str,
355        span: Option<Span>,
356        is_async: bool,
357        is_unsafe: bool,
358        visibility: Option<&str>,
359        signature: Option<&str>,
360    ) -> NodeId {
361        self.add_node_internal(
362            qualified_name,
363            span,
364            NodeKind::Function,
365            &[("async", is_async), ("unsafe", is_unsafe)],
366            visibility,
367            signature,
368            true,
369        )
370    }
371
372    /// Add a method node with the given qualified name.
373    pub fn add_method(
374        &mut self,
375        qualified_name: &str,
376        span: Option<Span>,
377        is_async: bool,
378        is_static: bool,
379    ) -> NodeId {
380        // Dual-use bare helper (issue #394): also used for call/callee stubs
381        // (servicenow/apex callees), so it defaults is_definition = false. Real
382        // declaration sites opt in via mark_definition (or the _with_* variants).
383        self.add_method_inner(qualified_name, span, is_async, is_static, false)
384    }
385
386    /// Internal method-node sink shared by the public declaration helper
387    /// [`add_method`](Self::add_method) (`is_definition = true`) and the
388    /// call-edge wrapper [`ensure_method`](Self::ensure_method)
389    /// (`is_definition = false`). See [`add_function_inner`](Self::add_function_inner).
390    fn add_method_inner(
391        &mut self,
392        qualified_name: &str,
393        span: Option<Span>,
394        is_async: bool,
395        is_static: bool,
396        is_definition: bool,
397    ) -> NodeId {
398        self.add_node_internal(
399            qualified_name,
400            span,
401            NodeKind::Method,
402            &[("async", is_async), ("static", is_static)],
403            None,
404            None,
405            is_definition,
406        )
407    }
408
409    /// Add a method node with visibility.
410    pub fn add_method_with_visibility(
411        &mut self,
412        qualified_name: &str,
413        span: Option<Span>,
414        is_async: bool,
415        is_static: bool,
416        visibility: Option<&str>,
417    ) -> NodeId {
418        self.add_node_internal(
419            qualified_name,
420            span,
421            NodeKind::Method,
422            &[("async", is_async), ("static", is_static)],
423            visibility,
424            None,
425            true,
426        )
427    }
428
429    /// Add a method node with signature (return type).
430    ///
431    /// The signature is used for `returns:` queries.
432    /// Returns the `NodeId` (creating the node if it doesn't exist).
433    pub fn add_method_with_signature(
434        &mut self,
435        qualified_name: &str,
436        span: Option<Span>,
437        is_async: bool,
438        is_static: bool,
439        visibility: Option<&str>,
440        signature: Option<&str>,
441    ) -> NodeId {
442        self.add_node_internal(
443            qualified_name,
444            span,
445            NodeKind::Method,
446            &[("async", is_async), ("static", is_static)],
447            visibility,
448            signature,
449            true,
450        )
451    }
452
453    /// Add a class node.
454    ///
455    /// Dual-use bare helper (issue #394): also used to create type-reference
456    /// stubs (e.g. puppet inherited-class targets), so it defaults
457    /// `is_definition = false`. Real declaration sites opt in via
458    /// [`mark_definition`](Self::mark_definition) (or use
459    /// [`add_class_with_visibility`](Self::add_class_with_visibility)).
460    pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
461        self.add_node_internal(
462            qualified_name,
463            span,
464            NodeKind::Class,
465            &[],
466            None,
467            None,
468            false,
469        )
470    }
471
472    /// Add a class node with visibility.
473    pub fn add_class_with_visibility(
474        &mut self,
475        qualified_name: &str,
476        span: Option<Span>,
477        visibility: Option<&str>,
478    ) -> NodeId {
479        self.add_node_internal(
480            qualified_name,
481            span,
482            NodeKind::Class,
483            &[],
484            visibility,
485            None,
486            true,
487        )
488    }
489
490    /// Add a struct node.
491    ///
492    /// Dual-use bare helper (issue #394): also used to create reference stubs
493    /// (e.g. go embedded-parent-struct targets), so it defaults
494    /// `is_definition = false`. Real declaration sites opt in via
495    /// [`mark_definition`](Self::mark_definition) (or use
496    /// [`add_struct_with_visibility`](Self::add_struct_with_visibility)).
497    pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
498        self.add_node_internal(
499            qualified_name,
500            span,
501            NodeKind::Struct,
502            &[],
503            None,
504            None,
505            false,
506        )
507    }
508
509    /// Add a struct node with visibility.
510    pub fn add_struct_with_visibility(
511        &mut self,
512        qualified_name: &str,
513        span: Option<Span>,
514        visibility: Option<&str>,
515    ) -> NodeId {
516        self.add_node_internal(
517            qualified_name,
518            span,
519            NodeKind::Struct,
520            &[],
521            visibility,
522            None,
523            true,
524        )
525    }
526
527    /// Add a module node.
528    ///
529    /// Dual-use bare helper (issue #394): also used to create FFI/import-target
530    /// stubs (e.g. python/kotlin native targets), so it defaults
531    /// `is_definition = false`. Real module-declaration sites opt in via
532    /// [`mark_definition`](Self::mark_definition).
533    pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
534        self.add_node_internal(
535            qualified_name,
536            span,
537            NodeKind::Module,
538            &[],
539            None,
540            None,
541            false,
542        )
543    }
544
545    /// Add a resource node.
546    pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
547        self.add_node_internal(
548            qualified_name,
549            span,
550            NodeKind::Resource,
551            &[],
552            None,
553            None,
554            true,
555        )
556    }
557
558    /// Add an endpoint node for HTTP route handlers.
559    ///
560    /// The qualified name should follow the convention `route::{METHOD}::{path}`,
561    /// for example `route::GET::/api/users` or `route::POST::/api/items`.
562    ///
563    /// Endpoint nodes are used by Pass 5 (cross-language linking) to match
564    /// HTTP requests from client code to server-side route handlers.
565    pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
566        self.add_node_internal(
567            qualified_name,
568            span,
569            NodeKind::Endpoint,
570            &[],
571            None,
572            None,
573            true,
574        )
575    }
576
577    /// Add an import node.
578    pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
579        self.add_node_internal(
580            qualified_name,
581            span,
582            NodeKind::Import,
583            &[],
584            None,
585            None,
586            false,
587        )
588    }
589
590    /// Add an import node while preserving the original path-like identifier.
591    ///
592    /// Use this for resource imports such as `styles.css`, `app.js`, or
593    /// similar asset filenames where `.` is part of the path rather than a
594    /// language-native qualified-name separator.
595    pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
596        self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None, false)
597    }
598
599    /// Add a variable node.
600    ///
601    /// Dual-use bare helper (issue #394): also used to create reference stubs
602    /// (e.g. rust field targets, sap-abap typed references), so it defaults
603    /// `is_definition = false`. Real variable/parameter/field declaration sites
604    /// opt in via [`mark_definition`](Self::mark_definition).
605    pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
606        self.add_node_internal(
607            qualified_name,
608            span,
609            NodeKind::Variable,
610            &[],
611            None,
612            None,
613            false,
614        )
615    }
616
617    /// Add a variable node while preserving the original identifier exactly.
618    ///
619    /// Use this for static asset references where the literal path is the
620    /// graph identity.
621    pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
622        self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None, false)
623    }
624
625    /// Add a constant node.
626    pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
627        self.add_node_internal(
628            qualified_name,
629            span,
630            NodeKind::Constant,
631            &[],
632            None,
633            None,
634            true,
635        )
636    }
637
638    /// Add a constant node with visibility.
639    pub fn add_constant_with_visibility(
640        &mut self,
641        qualified_name: &str,
642        span: Option<Span>,
643        visibility: Option<&str>,
644    ) -> NodeId {
645        self.add_node_internal(
646            qualified_name,
647            span,
648            NodeKind::Constant,
649            &[],
650            visibility,
651            None,
652            true,
653        )
654    }
655
656    /// Add a constant node with static and visibility attributes.
657    pub fn add_constant_with_static_and_visibility(
658        &mut self,
659        qualified_name: &str,
660        span: Option<Span>,
661        is_static: bool,
662        visibility: Option<&str>,
663    ) -> NodeId {
664        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
665        self.add_node_internal(
666            qualified_name,
667            span,
668            NodeKind::Constant,
669            attrs,
670            visibility,
671            None,
672            true,
673        )
674    }
675
676    /// Add a constant node with an explicit simple semantic name.
677    ///
678    /// Use this when the graph identity must keep a language-specific
679    /// qualified form but the searchable symbol name is the bare declaration
680    /// name.
681    pub fn add_constant_with_name_static_and_visibility(
682        &mut self,
683        name: &str,
684        qualified_name: &str,
685        span: Option<Span>,
686        is_static: bool,
687        visibility: Option<&str>,
688    ) -> NodeId {
689        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
690        self.add_node_internal_with_name(
691            name,
692            qualified_name,
693            span,
694            NodeKind::Constant,
695            attrs,
696            visibility,
697            None,
698            true,
699        )
700    }
701
702    /// Add a property node with static and visibility attributes.
703    pub fn add_property_with_static_and_visibility(
704        &mut self,
705        qualified_name: &str,
706        span: Option<Span>,
707        is_static: bool,
708        visibility: Option<&str>,
709    ) -> NodeId {
710        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
711        self.add_node_internal(
712            qualified_name,
713            span,
714            NodeKind::Property,
715            attrs,
716            visibility,
717            None,
718            true,
719        )
720    }
721
722    /// Add a property node with an explicit simple semantic name.
723    ///
724    /// Use this when the graph identity must keep a language-specific
725    /// qualified form but the searchable symbol name is the bare declaration
726    /// name.
727    pub fn add_property_with_name_static_and_visibility(
728        &mut self,
729        name: &str,
730        qualified_name: &str,
731        span: Option<Span>,
732        is_static: bool,
733        visibility: Option<&str>,
734    ) -> NodeId {
735        let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
736        self.add_node_internal_with_name(
737            name,
738            qualified_name,
739            span,
740            NodeKind::Property,
741            attrs,
742            visibility,
743            None,
744            true,
745        )
746    }
747
748    /// Add an enum node.
749    pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
750        self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None, true)
751    }
752
753    /// Add an enum node with visibility.
754    pub fn add_enum_with_visibility(
755        &mut self,
756        qualified_name: &str,
757        span: Option<Span>,
758        visibility: Option<&str>,
759    ) -> NodeId {
760        self.add_node_internal(
761            qualified_name,
762            span,
763            NodeKind::Enum,
764            &[],
765            visibility,
766            None,
767            true,
768        )
769    }
770
771    /// Add an interface/trait node.
772    ///
773    /// Dual-use bare helper (issue #394): also used to create type-reference
774    /// stubs (e.g. go interface-type references), so it defaults
775    /// `is_definition = false`. Real declaration sites opt in via
776    /// [`mark_definition`](Self::mark_definition) (or use
777    /// [`add_interface_with_visibility`](Self::add_interface_with_visibility)).
778    pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
779        self.add_node_internal(
780            qualified_name,
781            span,
782            NodeKind::Interface,
783            &[],
784            None,
785            None,
786            false,
787        )
788    }
789
790    /// Add an interface/trait node with visibility.
791    pub fn add_interface_with_visibility(
792        &mut self,
793        qualified_name: &str,
794        span: Option<Span>,
795        visibility: Option<&str>,
796    ) -> NodeId {
797        self.add_node_internal(
798            qualified_name,
799            span,
800            NodeKind::Interface,
801            &[],
802            visibility,
803            None,
804            true,
805        )
806    }
807
808    /// Add a type alias node.
809    ///
810    /// Irreducibly dual-use bare helper (issue #394): used for BOTH typedef/
811    /// type-alias declarations AND type references (the dominant use), so it
812    /// defaults `is_definition = false`. A type DECLARED in the workspace opts
813    /// in at its declaration site via [`mark_definition`](Self::mark_definition)
814    /// (references then dedupe into it and the OR-in keeps it true); a type only
815    /// ever referenced stays false.
816    pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
817        self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None, false)
818    }
819
820    /// Add a type alias node with visibility.
821    pub fn add_type_with_visibility(
822        &mut self,
823        qualified_name: &str,
824        span: Option<Span>,
825        visibility: Option<&str>,
826    ) -> NodeId {
827        self.add_node_internal(
828            qualified_name,
829            span,
830            NodeKind::Type,
831            &[],
832            visibility,
833            None,
834            true,
835        )
836    }
837
838    /// Add a lifetime node.
839    pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
840        self.add_node_internal(
841            qualified_name,
842            span,
843            NodeKind::Lifetime,
844            &[],
845            None,
846            None,
847            true,
848        )
849    }
850
851    /// Add a lifetime constraint edge.
852    pub fn add_lifetime_constraint_edge(
853        &mut self,
854        source: NodeId,
855        target: NodeId,
856        constraint_kind: LifetimeConstraintKind,
857    ) {
858        self.staging.add_edge(
859            source,
860            target,
861            EdgeKind::LifetimeConstraint { constraint_kind },
862            self.file_id,
863        );
864    }
865
866    /// Add a trait method binding edge.
867    ///
868    /// This edge represents the resolution of a trait method call to a concrete
869    /// implementation.
870    pub fn add_trait_method_binding_edge(
871        &mut self,
872        caller: NodeId,
873        callee: NodeId,
874        trait_name: &str,
875        impl_type: &str,
876        is_ambiguous: bool,
877    ) {
878        let trait_name_id = self.intern(trait_name);
879        let impl_type_id = self.intern(impl_type);
880        self.staging.add_edge(
881            caller,
882            callee,
883            EdgeKind::TraitMethodBinding {
884                trait_name: trait_name_id,
885                impl_type: impl_type_id,
886                is_ambiguous,
887            },
888            self.file_id,
889        );
890    }
891
892    /// Add a macro expansion edge.
893    ///
894    /// Represents the expansion of a macro invocation to its generated code.
895    /// Only available when macro expansion is enabled.
896    ///
897    /// # Arguments
898    ///
899    /// * `invocation` - The macro invocation site node (e.g., derive attribute or macro call)
900    /// * `expansion` - The macro definition or generated code node
901    /// * `expansion_kind` - The kind of macro expansion (Derive, Attribute, Declarative, Function)
902    /// * `is_verified` - Whether the expansion has been verified (requires `cargo expand`)
903    ///
904    /// # Example
905    ///
906    /// ```ignore
907    /// // #[derive(Debug)] on a struct
908    /// let struct_id = helper.add_struct("MyStruct", Some(span));
909    /// let derive_macro_id = helper.add_node("MyStruct::derive_Debug", None, NodeKind::Macro);
910    /// helper.add_macro_expansion_edge(
911    ///     struct_id,
912    ///     derive_macro_id,
913    ///     MacroExpansionKind::Derive,
914    ///     false,
915    /// );
916    /// ```
917    pub fn add_macro_expansion_edge(
918        &mut self,
919        invocation: NodeId,
920        expansion: NodeId,
921        expansion_kind: MacroExpansionKind,
922        is_verified: bool,
923    ) {
924        self.staging.add_edge(
925            invocation,
926            expansion,
927            EdgeKind::MacroExpansion {
928                expansion_kind,
929                is_verified,
930            },
931            self.file_id,
932        );
933    }
934
935    /// Add a generic node with custom kind.
936    ///
937    /// Generic nodes default to `is_definition = false`: the caller passes a raw
938    /// `NodeKind` so the helper cannot know whether the node is a real
939    /// declaration or a structural/reference stub. Real-declaration callers are
940    /// marked explicitly in Stage 2.
941    pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
942        self.add_node_internal(qualified_name, span, kind, &[], None, None, false)
943    }
944
945    /// Add a generic node with visibility.
946    ///
947    /// Defaults to `is_definition = false` for the same reason as
948    /// [`add_node`](Self::add_node).
949    pub fn add_node_with_visibility(
950        &mut self,
951        qualified_name: &str,
952        span: Option<Span>,
953        kind: NodeKind,
954        visibility: Option<&str>,
955    ) -> NodeId {
956        self.add_node_internal(qualified_name, span, kind, &[], visibility, None, false)
957    }
958
959    /// Mark a just-created staged node as a real source declaration
960    /// (`is_definition = true`).
961    ///
962    /// This is the explicit opt-in (issue #394) for declaration sites that
963    /// create their node through a DUAL-USE bare helper (`add_function`,
964    /// `add_method`, `add_class`, `add_struct`, `add_enum`-less kinds aside,
965    /// `add_interface`, `add_type`, `add_module`, `add_variable`) or the generic
966    /// `add_node`/`add_node_with_visibility`, all of which default
967    /// `is_definition = false` because the helper cannot tell a declaration from
968    /// a call/FFI/reference/import stub. A declaration handler calls this right
969    /// after creating its node.
970    ///
971    /// The signal is monotonic (OR-in): once marked true it is never cleared, so
972    /// calling this on a node that was also reached as a stub (or vice-versa)
973    /// converges to true, which is correct (a symbol declared in the workspace
974    /// IS a definition regardless of also being referenced).
975    pub fn mark_definition(&mut self, node_id: NodeId) {
976        let update = NodeMetadataUpdate::new().mark_if(NodeMetadataFlag::Definition, true);
977        self.staging.update_node_entry(node_id, &update);
978    }
979
980    /// Internal helper for adding nodes.
981    ///
982    /// Applies attributes to the node entry:
983    /// - `"async"` → `NodeEntry::with_async(true/false)`
984    /// - `"static"` → `NodeEntry::with_static(true/false)`
985    /// - `"unsafe"` → `NodeEntry::with_unsafe(true/false)`
986    ///
987    /// When `signature` is `Some`, the signature field is set on the node for
988    /// `returns:` queries.
989    fn add_node_internal(
990        &mut self,
991        qualified_name: &str,
992        span: Option<Span>,
993        kind: NodeKind,
994        attributes: &[(&str, bool)],
995        visibility: Option<&str>,
996        signature: Option<&str>,
997        is_definition: bool,
998    ) -> NodeId {
999        let canonical_qualified_name =
1000            canonicalize_graph_qualified_name(self.language, qualified_name);
1001        let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
1002        self.add_node_internal_with_canonical_name(
1003            &semantic_name,
1004            &canonical_qualified_name,
1005            span,
1006            kind,
1007            attributes,
1008            visibility,
1009            signature,
1010            is_definition,
1011        )
1012    }
1013
1014    #[allow(clippy::too_many_arguments)] // internal builder sink; is_definition (issue #394) threaded alongside span/kind/attrs
1015    fn add_node_internal_with_name(
1016        &mut self,
1017        semantic_name: &str,
1018        qualified_name: &str,
1019        span: Option<Span>,
1020        kind: NodeKind,
1021        attributes: &[(&str, bool)],
1022        visibility: Option<&str>,
1023        signature: Option<&str>,
1024        is_definition: bool,
1025    ) -> NodeId {
1026        let canonical_qualified_name =
1027            canonicalize_graph_qualified_name(self.language, qualified_name);
1028        self.add_node_internal_with_canonical_name(
1029            semantic_name,
1030            &canonical_qualified_name,
1031            span,
1032            kind,
1033            attributes,
1034            visibility,
1035            signature,
1036            is_definition,
1037        )
1038    }
1039
1040    #[allow(clippy::too_many_arguments)] // internal builder sink; is_definition (issue #394) threaded alongside span/kind/attrs
1041    fn add_node_internal_with_canonical_name(
1042        &mut self,
1043        semantic_name: &str,
1044        canonical_qualified_name: &str,
1045        span: Option<Span>,
1046        kind: NodeKind,
1047        attributes: &[(&str, bool)],
1048        visibility: Option<&str>,
1049        signature: Option<&str>,
1050        is_definition: bool,
1051    ) -> NodeId {
1052        let mut is_async = false;
1053        let mut is_static = false;
1054        let mut is_unsafe = false;
1055        for &(key, value) in attributes {
1056            match key {
1057                "async" => is_async |= value,
1058                "static" => is_static |= value,
1059                "unsafe" => is_unsafe |= value,
1060                _ => {}
1061            }
1062        }
1063
1064        // Check cache first
1065        if let Some(&id) = self
1066            .node_cache
1067            .get(&(canonical_qualified_name.to_string(), kind))
1068        {
1069            let visibility_id = visibility.map(|vis| self.intern(vis));
1070            let signature_id = signature.map(|sig| self.intern(sig));
1071            let update = NodeMetadataUpdate::new()
1072                .with_optional_span(span)
1073                .mark_if(NodeMetadataFlag::Async, is_async)
1074                .mark_if(NodeMetadataFlag::Static, is_static)
1075                .mark_if(NodeMetadataFlag::Unsafe, is_unsafe)
1076                .mark_if(NodeMetadataFlag::Definition, is_definition)
1077                .with_optional_visibility(visibility_id)
1078                .with_optional_signature(signature_id);
1079            self.staging.update_node_entry(id, &update);
1080            return id;
1081        }
1082
1083        let name_id = self.intern(semantic_name);
1084
1085        // Create node entry
1086        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
1087        entry.is_definition = is_definition;
1088        if semantic_name != canonical_qualified_name {
1089            let qualified_name_id = self.intern(canonical_qualified_name);
1090            entry = entry.with_qualified_name(qualified_name_id);
1091        }
1092
1093        // Set span if provided
1094        if let Some(s) = span {
1095            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
1096            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
1097            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
1098            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
1099            entry = entry.with_location(start_line, start_column, end_line, end_column);
1100        }
1101
1102        // Apply attributes to node entry
1103        if is_async {
1104            entry = entry.with_async(true);
1105        }
1106        if is_static {
1107            entry = entry.with_static(true);
1108        }
1109        if is_unsafe {
1110            entry = entry.with_unsafe(true);
1111        }
1112
1113        // Apply visibility if provided
1114        if let Some(vis) = visibility {
1115            let vis_id = self.intern(vis);
1116            entry = entry.with_visibility(vis_id);
1117        }
1118
1119        // Apply signature (return type) if provided
1120        if let Some(sig) = signature {
1121            let sig_id = self.intern(sig);
1122            entry = entry.with_signature(sig_id);
1123        }
1124
1125        // Stage the node
1126        let node_id = self.staging.add_node(entry);
1127
1128        // Cache for deduplication
1129        self.node_cache
1130            .insert((canonical_qualified_name.to_string(), kind), node_id);
1131
1132        node_id
1133    }
1134
1135    fn add_node_verbatim(
1136        &mut self,
1137        name: &str,
1138        span: Option<Span>,
1139        kind: NodeKind,
1140        attributes: &[(&str, bool)],
1141        visibility: Option<&str>,
1142        signature: Option<&str>,
1143        is_definition: bool,
1144    ) -> NodeId {
1145        let mut is_async = false;
1146        let mut is_static = false;
1147        let mut is_unsafe = false;
1148        for &(key, value) in attributes {
1149            match key {
1150                "async" => is_async |= value,
1151                "static" => is_static |= value,
1152                "unsafe" => is_unsafe |= value,
1153                _ => {}
1154            }
1155        }
1156
1157        if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
1158            let visibility_id = visibility.map(|vis| self.intern(vis));
1159            let signature_id = signature.map(|sig| self.intern(sig));
1160            let update = NodeMetadataUpdate::new()
1161                .with_optional_span(span)
1162                .mark_if(NodeMetadataFlag::Async, is_async)
1163                .mark_if(NodeMetadataFlag::Static, is_static)
1164                .mark_if(NodeMetadataFlag::Unsafe, is_unsafe)
1165                .mark_if(NodeMetadataFlag::Definition, is_definition)
1166                .with_optional_visibility(visibility_id)
1167                .with_optional_signature(signature_id);
1168            self.staging.update_node_entry(id, &update);
1169            return id;
1170        }
1171
1172        let name_id = self.intern(name);
1173        let mut entry = NodeEntry::new(kind, name_id, self.file_id);
1174        entry.is_definition = is_definition;
1175
1176        if let Some(s) = span {
1177            let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
1178            let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
1179            let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
1180            let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
1181            entry = entry.with_location(start_line, start_column, end_line, end_column);
1182        }
1183
1184        if is_async {
1185            entry = entry.with_async(true);
1186        }
1187        if is_static {
1188            entry = entry.with_static(true);
1189        }
1190        if is_unsafe {
1191            entry = entry.with_unsafe(true);
1192        }
1193
1194        if let Some(vis) = visibility {
1195            let vis_id = self.intern(vis);
1196            entry = entry.with_visibility(vis_id);
1197        }
1198        if let Some(sig) = signature {
1199            let sig_id = self.intern(sig);
1200            entry = entry.with_signature(sig_id);
1201        }
1202
1203        let node_id = self.staging.add_node(entry);
1204        self.node_cache.insert((name.to_string(), kind), node_id);
1205        node_id
1206    }
1207
1208    /// Add a call edge from caller to callee.
1209    pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
1210        self.add_call_edge_with_span(caller, callee, Vec::new());
1211    }
1212
1213    /// Add a call edge from caller to callee with source span information.
1214    ///
1215    /// The span should point to the call site location in source code.
1216    ///
1217    /// # Note
1218    ///
1219    /// This method uses default metadata (`argument_count: 255` sentinel for unknown, `is_async: false`).
1220    /// Use [`add_call_edge_full`](Self::add_call_edge_full) when you need to specify
1221    /// argument count or async status explicitly.
1222    pub fn add_call_edge_with_span(
1223        &mut self,
1224        caller: NodeId,
1225        callee: NodeId,
1226        spans: Vec<crate::graph::node::Span>,
1227    ) {
1228        self.staging.add_edge_with_spans(
1229            caller,
1230            callee,
1231            EdgeKind::Calls {
1232                argument_count: 255,
1233                is_async: false,
1234                resolved_via: ResolvedVia::Direct,
1235            },
1236            self.file_id,
1237            spans,
1238        );
1239    }
1240
1241    /// Add a call edge with full metadata.
1242    ///
1243    /// Use this method when you know the argument count or when the call is async.
1244    /// For calls where metadata is unknown, use [`add_call_edge`](Self::add_call_edge)
1245    /// which uses default values (`argument_count: 255` sentinel, `is_async: false`).
1246    ///
1247    /// # Arguments
1248    ///
1249    /// * `caller` - The node making the call
1250    /// * `callee` - The node being called
1251    /// * `argument_count` - Number of arguments in the call (0-254, use 255 for unknown)
1252    /// * `is_async` - Whether this is an async/await call
1253    ///
1254    /// # Canonical Usage
1255    ///
1256    /// | Scenario | Method |
1257    /// |----------|--------|
1258    /// | Argument count known, sync call | `add_call_edge_full(caller, callee, arg_count, false)` |
1259    /// | Argument count known, async call | `add_call_edge_full(caller, callee, arg_count, true)` |
1260    /// | Argument count unknown, sync call | `add_call_edge(caller, callee)` or `add_call_edge_full(caller, callee, 255, false)` |
1261    ///
1262    /// # Example
1263    ///
1264    /// ```ignore
1265    /// // Function call with 3 arguments
1266    /// helper.add_call_edge_full(main_id, helper_id, 3, false);
1267    ///
1268    /// // Async call with 1 argument
1269    /// helper.add_call_edge_full(main_id, async_fn_id, 1, true);
1270    /// ```
1271    pub fn add_call_edge_full(
1272        &mut self,
1273        caller: NodeId,
1274        callee: NodeId,
1275        argument_count: u8,
1276        is_async: bool,
1277    ) {
1278        self.staging.add_edge(
1279            caller,
1280            callee,
1281            EdgeKind::Calls {
1282                argument_count,
1283                is_async,
1284                resolved_via: ResolvedVia::Direct,
1285            },
1286            self.file_id,
1287        );
1288    }
1289
1290    /// Add a call edge with full metadata and source span information.
1291    ///
1292    /// Combines the functionality of [`add_call_edge_full`](Self::add_call_edge_full)
1293    /// and span tracking.
1294    pub fn add_call_edge_full_with_span(
1295        &mut self,
1296        caller: NodeId,
1297        callee: NodeId,
1298        argument_count: u8,
1299        is_async: bool,
1300        spans: Vec<crate::graph::node::Span>,
1301    ) {
1302        self.staging.add_edge_with_spans(
1303            caller,
1304            callee,
1305            EdgeKind::Calls {
1306                argument_count,
1307                is_async,
1308                resolved_via: ResolvedVia::Direct,
1309            },
1310            self.file_id,
1311            spans,
1312        );
1313    }
1314
1315    /// Stage a `NodeKind::Channel` node for a Go channel alias-class (T2.4).
1316    ///
1317    /// Dedupes by `(qualified_name, NodeKind::Channel)` through the canonical
1318    /// node cache, so all operation sites on the same alias-class collapse to
1319    /// one node. `buffer_kind` / `capacity` classify the channel's
1320    /// `make(chan T, N)` form; in Phase 1 the classifier is carried onto each
1321    /// `ChannelPeer` edge (where the planner consumes it) rather than into a
1322    /// separate node-metadata payload, so the parameters document the
1323    /// classification at the call site without a persistence-format addition.
1324    pub fn add_channel(
1325        &mut self,
1326        qualified_name: &str,
1327        span: Option<Span>,
1328        _buffer_kind: ChannelBufferKind,
1329        _capacity: Option<u32>,
1330    ) -> NodeId {
1331        self.add_node(qualified_name, span, NodeKind::Channel)
1332    }
1333
1334    /// Stage a `ChannelPeer` edge from an operation-site `CallSite` to its
1335    /// canonical `Channel` node (T2.4).
1336    pub fn add_channel_peer_edge_with_span(
1337        &mut self,
1338        op_site: NodeId,
1339        channel: NodeId,
1340        direction: ChannelPeerDirection,
1341        buffer_kind: ChannelBufferKind,
1342        span: Span,
1343    ) {
1344        self.staging.add_edge_with_spans(
1345            op_site,
1346            channel,
1347            EdgeKind::ChannelPeer {
1348                direction,
1349                buffer_kind,
1350            },
1351            self.file_id,
1352            vec![span],
1353        );
1354    }
1355
1356    /// Stage an `Instantiates` edge from a generic call-site `CallSite` to the
1357    /// generic function / method definition (T2.5).
1358    ///
1359    /// `type_args` is taken by value so the Go plugin can build it via
1360    /// `SmallVec::from_iter(...)` and move ownership. The `TypeArg.name`
1361    /// `StringId`s are interned through this helper's interner, so they are
1362    /// remapped to global ids during the commit's string-table dedup.
1363    pub fn add_instantiates_edge_with_span(
1364        &mut self,
1365        call_site: NodeId,
1366        target: NodeId,
1367        type_args: smallvec::SmallVec<[TypeArg; 4]>,
1368        inference_kind: InferenceKind,
1369        span: Span,
1370    ) {
1371        self.staging.add_edge_with_spans(
1372            call_site,
1373            target,
1374            EdgeKind::Instantiates {
1375                type_args,
1376                inference_kind,
1377            },
1378            self.file_id,
1379            vec![span],
1380        );
1381    }
1382
1383    /// Add a database table read edge (SQL).
1384    pub fn add_table_read_edge_with_span(
1385        &mut self,
1386        reader: NodeId,
1387        table: NodeId,
1388        table_name: &str,
1389        schema: Option<&str>,
1390        spans: Vec<crate::graph::node::Span>,
1391    ) {
1392        let table_name_id = self.intern(table_name);
1393        let schema_id = schema.map(|s| self.intern(s));
1394        self.staging.add_edge_with_spans(
1395            reader,
1396            table,
1397            EdgeKind::TableRead {
1398                table_name: table_name_id,
1399                schema: schema_id,
1400            },
1401            self.file_id,
1402            spans,
1403        );
1404    }
1405
1406    /// Add a database table write edge (SQL).
1407    pub fn add_table_write_edge_with_span(
1408        &mut self,
1409        writer: NodeId,
1410        table: NodeId,
1411        table_name: &str,
1412        schema: Option<&str>,
1413        operation: TableWriteOp,
1414        spans: Vec<crate::graph::node::Span>,
1415    ) {
1416        let table_name_id = self.intern(table_name);
1417        let schema_id = schema.map(|s| self.intern(s));
1418        self.staging.add_edge_with_spans(
1419            writer,
1420            table,
1421            EdgeKind::TableWrite {
1422                table_name: table_name_id,
1423                schema: schema_id,
1424                operation,
1425            },
1426            self.file_id,
1427            spans,
1428        );
1429    }
1430
1431    /// Add a database trigger relationship edge (SQL).
1432    ///
1433    /// Convention: `trigger -> table` with `EdgeKind::TriggeredBy`.
1434    pub fn add_triggered_by_edge_with_span(
1435        &mut self,
1436        trigger: NodeId,
1437        table: NodeId,
1438        trigger_name: &str,
1439        schema: Option<&str>,
1440        spans: Vec<crate::graph::node::Span>,
1441    ) {
1442        let trigger_name_id = self.intern(trigger_name);
1443        let schema_id = schema.map(|s| self.intern(s));
1444        self.staging.add_edge_with_spans(
1445            trigger,
1446            table,
1447            EdgeKind::TriggeredBy {
1448                trigger_name: trigger_name_id,
1449                schema: schema_id,
1450            },
1451            self.file_id,
1452            spans,
1453        );
1454    }
1455
1456    /// Add an import edge from importer to imported module/symbol.
1457    ///
1458    /// This method uses default metadata (`alias: None`, `is_wildcard: false`).
1459    /// Use [`add_import_edge_full`](Self::add_import_edge_full) when importing
1460    /// with an alias or for wildcard imports.
1461    pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
1462        self.staging.add_edge(
1463            importer,
1464            imported,
1465            EdgeKind::Imports {
1466                alias: None,
1467                is_wildcard: false,
1468            },
1469            self.file_id,
1470        );
1471    }
1472
1473    /// Add an import edge with full metadata.
1474    ///
1475    /// Use this method when the import has an alias or is a wildcard import.
1476    /// For simple imports without alias or wildcard, use [`add_import_edge`](Self::add_import_edge).
1477    ///
1478    /// # Arguments
1479    ///
1480    /// * `importer` - The node importing (e.g., module or file)
1481    /// * `imported` - The node being imported
1482    /// * `alias` - Optional alias string (e.g., for `import { foo as bar }`, alias is "bar")
1483    /// * `is_wildcard` - Whether this is a wildcard import (e.g., `import *`)
1484    ///
1485    /// # Canonical Usage
1486    ///
1487    /// | Import Syntax | Method |
1488    /// |---------------|--------|
1489    /// | `import foo` | `add_import_edge(importer, imported)` |
1490    /// | `import foo as bar` | `add_import_edge_full(importer, imported, Some("bar"), false)` |
1491    /// | `import *` / `import *.*` | `add_import_edge_full(importer, imported, None, true)` |
1492    /// | `import * as ns` | `add_import_edge_full(importer, imported, Some("ns"), true)` |
1493    ///
1494    /// # Example
1495    ///
1496    /// ```ignore
1497    /// // import { HashMap as Map } from "std::collections"
1498    /// let alias_id = helper.intern("Map");
1499    /// helper.add_import_edge_full(module_id, hashmap_id, Some("Map"), false);
1500    ///
1501    /// // import * from "lodash"
1502    /// helper.add_import_edge_full(module_id, lodash_id, None, true);
1503    /// ```
1504    pub fn add_import_edge_full(
1505        &mut self,
1506        importer: NodeId,
1507        imported: NodeId,
1508        alias: Option<&str>,
1509        is_wildcard: bool,
1510    ) {
1511        let alias_id = alias.map(|s| self.intern(s));
1512        self.staging.add_edge(
1513            importer,
1514            imported,
1515            EdgeKind::Imports {
1516                alias: alias_id,
1517                is_wildcard,
1518            },
1519            self.file_id,
1520        );
1521    }
1522
1523    /// Add an export edge from module to exported symbol.
1524    ///
1525    /// This method uses default metadata (`kind: ExportKind::Direct`, `alias: None`).
1526    /// Use [`add_export_edge_full`](Self::add_export_edge_full) for re-exports,
1527    /// default exports, namespace exports, or exports with aliases.
1528    pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1529        self.staging.add_edge(
1530            module,
1531            exported,
1532            EdgeKind::Exports {
1533                kind: ExportKind::Direct,
1534                alias: None,
1535            },
1536            self.file_id,
1537        );
1538    }
1539
1540    /// Add an export edge with full metadata.
1541    ///
1542    /// Use this method for re-exports, default exports, namespace exports,
1543    /// or exports with aliases. For simple direct exports without alias,
1544    /// use [`add_export_edge`](Self::add_export_edge).
1545    ///
1546    /// # Arguments
1547    ///
1548    /// * `module` - The module/file node that contains the export
1549    /// * `exported` - The symbol being exported
1550    /// * `kind` - The kind of export:
1551    ///   - `ExportKind::Direct` - Direct export (`export { foo }`)
1552    ///   - `ExportKind::Reexport` - Re-export from another module (`export { foo } from "mod"`)
1553    ///   - `ExportKind::Default` - Default export (`export default foo`)
1554    ///   - `ExportKind::Namespace` - Namespace export (`export * as ns from "mod"`)
1555    /// * `alias` - Optional alias string (e.g., for `export { foo as bar }`, alias is "bar")
1556    ///
1557    /// # Canonical Usage
1558    ///
1559    /// | Export Syntax (JS/TS) | Method |
1560    /// |-----------------------|--------|
1561    /// | `export { name }` | `add_export_edge(module, name)` |
1562    /// | `export default foo` | `add_export_edge_full(module, foo, ExportKind::Default, None)` |
1563    /// | `export { foo as bar }` | `add_export_edge_full(module, foo, ExportKind::Direct, Some("bar"))` |
1564    /// | `export { foo } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, None)` |
1565    /// | `export { foo as bar } from "mod"` | `add_export_edge_full(module, foo, ExportKind::Reexport, Some("bar"))` |
1566    /// | `export * from "mod"` | `add_export_edge_full(module, mod, ExportKind::Reexport, None)` |
1567    /// | `export * as ns from "mod"` | `add_export_edge_full(module, mod, ExportKind::Namespace, Some("ns"))` |
1568    ///
1569    /// # Example
1570    ///
1571    /// ```ignore
1572    /// // export default MyComponent;
1573    /// helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1574    ///
1575    /// // export { helper as utilHelper };
1576    /// helper.add_export_edge_full(module_id, helper_id, ExportKind::Direct, Some("utilHelper"));
1577    ///
1578    /// // export * as utils from "./utils";
1579    /// helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1580    /// ```
1581    pub fn add_export_edge_full(
1582        &mut self,
1583        module: NodeId,
1584        exported: NodeId,
1585        kind: ExportKind,
1586        alias: Option<&str>,
1587    ) {
1588        let alias_id = alias.map(|s| self.intern(s));
1589        self.staging.add_edge(
1590            module,
1591            exported,
1592            EdgeKind::Exports {
1593                kind,
1594                alias: alias_id,
1595            },
1596            self.file_id,
1597        );
1598    }
1599
1600    /// Add a reference edge (variable/field access).
1601    pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1602        self.staging
1603            .add_edge(from, to, EdgeKind::References, self.file_id);
1604    }
1605
1606    /// Add a defines edge (module defines symbol).
1607    pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1608        self.staging
1609            .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1610    }
1611
1612    /// Add a type-of edge (symbol has type).
1613    /// Add a `TypeOf` edge without context metadata (backward compatibility).
1614    ///
1615    /// For new code, prefer `add_typeof_edge_with_context` to provide semantic context.
1616    pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1617        self.add_typeof_edge_with_context(source, target, None, None, None);
1618    }
1619
1620    /// Add a `TypeOf` edge with optional context metadata.
1621    ///
1622    /// # Parameters
1623    /// - `source`: The node that has this type (e.g., variable, function, parameter)
1624    /// - `target`: The type node
1625    /// - `context`: Where this type reference appears (Parameter, Return, Field, Variable, etc.)
1626    /// - `index`: Position/index (for parameters, returns, fields)
1627    /// - `name`: Name (for parameters, returns, fields, variables)
1628    ///
1629    /// # Examples
1630    /// ```ignore
1631    /// // Function parameter: func foo(ctx context.Context)
1632    /// helper.add_typeof_edge_with_context(
1633    ///     func_id,
1634    ///     type_id,
1635    ///     Some(TypeOfContext::Parameter),
1636    ///     Some(0),
1637    ///     Some("ctx"),
1638    /// );
1639    ///
1640    /// // Function return: func bar() error
1641    /// helper.add_typeof_edge_with_context(
1642    ///     func_id,
1643    ///     error_type_id,
1644    ///     Some(TypeOfContext::Return),
1645    ///     Some(0),
1646    ///     None,
1647    /// );
1648    ///
1649    /// // Variable: var x int
1650    /// helper.add_typeof_edge_with_context(
1651    ///     var_id,
1652    ///     int_type_id,
1653    ///     Some(TypeOfContext::Variable),
1654    ///     None,
1655    ///     Some("x"),
1656    /// );
1657    /// ```
1658    pub fn add_typeof_edge_with_context(
1659        &mut self,
1660        source: NodeId,
1661        target: NodeId,
1662        context: Option<TypeOfContext>,
1663        index: Option<u16>,
1664        name: Option<&str>,
1665    ) {
1666        let name_id = name.map(|n| self.intern(n));
1667        self.staging.add_edge(
1668            source,
1669            target,
1670            EdgeKind::TypeOf {
1671                context,
1672                index,
1673                name: name_id,
1674            },
1675            self.file_id,
1676        );
1677    }
1678
1679    /// Add an implements edge (class implements interface).
1680    pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1681        self.staging
1682            .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1683    }
1684
1685    /// Add an inherits edge (class extends class).
1686    pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1687        self.staging
1688            .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1689    }
1690
1691    /// Add a T3 `Wraps` edge from a wrapper expression to a wrapped error
1692    /// value (Go error chains, `02_DESIGN` §1.3 / §2.4).
1693    ///
1694    /// The `kind` discriminates the source-syntax form
1695    /// (`fmt.Errorf("%w", err)`, `Unwrap()` method, `errors.{Is,As,AsType,Join}`);
1696    /// `chain_position` carries the verb index for `%w` (0-based, skipping
1697    /// `%%`) and the slice index for `errors.Join` / `Unwrap() []error`
1698    /// slice literals (`None` for single-value forms).
1699    ///
1700    /// `span` optionally records the source location of the wrap site
1701    /// (e.g. the `%w` verb or the `Unwrap()` call expression). Pass `None`
1702    /// when the caller cannot resolve a meaningful position. Wraps edges
1703    /// route through the existing staging `EdgeStore` exactly like
1704    /// [`Self::add_implements_edge`] — Phase 4d-prime is the only new
1705    /// pipeline structural change required by T3.
1706    pub fn add_wraps_edge(
1707        &mut self,
1708        source: NodeId,
1709        target: NodeId,
1710        kind: WrapKind,
1711        chain_position: Option<u16>,
1712        span: Option<Span>,
1713    ) {
1714        let spans = span.map(|s| vec![s]).unwrap_or_default();
1715        self.staging.add_edge_with_spans(
1716            source,
1717            target,
1718            EdgeKind::Wraps {
1719                kind,
1720                chain_position,
1721            },
1722            self.file_id,
1723            spans,
1724        );
1725    }
1726
1727    /// Add a contains edge (parent contains child, e.g., class contains method).
1728    pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1729        self.staging
1730            .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1731    }
1732
1733    /// Add a WebAssembly call edge.
1734    ///
1735    /// Used when JavaScript/TypeScript code instantiates or calls WebAssembly modules:
1736    /// - `WebAssembly.instantiate()` / `WebAssembly.instantiateStreaming()`
1737    /// - `new WebAssembly.Module()` / `new WebAssembly.Instance()`
1738    /// - Calling exported WASM functions
1739    pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1740        self.staging
1741            .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1742    }
1743
1744    /// Add an FFI call edge with the specified calling convention.
1745    ///
1746    /// Used for foreign function interface calls:
1747    /// - Node.js native addons (`.node` files)
1748    /// - ctypes/cffi in Python
1749    /// - JNI in Java
1750    /// - P/Invoke in C#
1751    pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1752        self.staging.add_edge(
1753            caller,
1754            ffi_target,
1755            EdgeKind::FfiCall { convention },
1756            self.file_id,
1757        );
1758    }
1759
1760    /// Add an HTTP request edge.
1761    ///
1762    /// Use this when detecting HTTP calls like `fetch()` or `axios.get()`.
1763    pub fn add_http_request_edge(
1764        &mut self,
1765        caller: NodeId,
1766        target: NodeId,
1767        method: HttpMethod,
1768        url: Option<&str>,
1769    ) {
1770        let url_id = url.map(|value| self.intern(value));
1771        self.staging.add_edge(
1772            caller,
1773            target,
1774            EdgeKind::HttpRequest {
1775                method,
1776                url: url_id,
1777            },
1778            self.file_id,
1779        );
1780    }
1781
1782    /// Search `CALL_COMPATIBLE_KINDS` for an existing node with the given
1783    /// canonical qualified name, skipping `exclude` (the caller's own kind).
1784    ///
1785    /// Returns the first matching `NodeId` or `None`. The sweep is read-only —
1786    /// no metadata is mutated on cross-kind reuse (Stage 1 declaration metadata
1787    /// is authoritative).
1788    fn reuse_across_call_compatible_kinds(
1789        &self,
1790        canonical: &str,
1791        exclude: NodeKind,
1792    ) -> Option<NodeId> {
1793        for &kind in CALL_COMPATIBLE_KINDS {
1794            if kind == exclude {
1795                continue;
1796            }
1797            if let Some(&id) = self.node_cache.get(&(canonical.to_string(), kind)) {
1798                return Some(id);
1799            }
1800        }
1801        None
1802    }
1803
1804    /// Ensure a callee node exists for call-edge construction, with a
1805    /// **non-optional** call-site span.
1806    ///
1807    /// This is the preferred API for Stage 2 call-edge building. The span is
1808    /// required so that every stub gets at least the caller's line — never 0.
1809    /// The `kind_hint` guides the sweep order and determines the `NodeKind`
1810    /// used if a fresh node must be created.
1811    ///
1812    /// Cross-kind reuse: if a node with the same canonical qualified name
1813    /// already exists as any call-compatible kind, it is returned as-is.
1814    pub fn ensure_callee(
1815        &mut self,
1816        qualified_name: &str,
1817        call_site_span: Span,
1818        kind_hint: CalleeKindHint,
1819    ) -> NodeId {
1820        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1821        let target_kind = kind_hint.to_node_kind();
1822
1823        // First check for exact-kind cache hit (fast path)
1824        if let Some(&id) = self.node_cache.get(&(canonical.clone(), target_kind)) {
1825            return id;
1826        }
1827        // Then sweep all other call-compatible kinds
1828        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, target_kind) {
1829            return id;
1830        }
1831        // Create a new node with the call-site span (never None). Callee stubs
1832        // are never declarations -> is_definition = false.
1833        self.add_node_internal(
1834            qualified_name,
1835            Some(call_site_span),
1836            target_kind,
1837            &[],
1838            None,
1839            None,
1840            false,
1841        )
1842    }
1843
1844    /// Ensure a function node exists, creating it if needed.
1845    ///
1846    /// Cross-kind reuse: if a node with the same canonical qualified name
1847    /// already exists as any call-compatible kind (Method, Macro, Constant,
1848    /// `LambdaTarget`), the existing node is returned as-is. This prevents
1849    /// duplicate spanless Function nodes from being created during Stage 2
1850    /// call-edge construction, which would cause `get_references` to silently
1851    /// drop callers due to location-based deduplication at `(file, line=0, col=0)`.
1852    ///
1853    /// The Stage 1 declaration node is authoritative for metadata — no attributes
1854    /// are mutated on cross-kind reuse.
1855    pub fn ensure_function(
1856        &mut self,
1857        qualified_name: &str,
1858        span: Option<Span>,
1859        is_async: bool,
1860        is_unsafe: bool,
1861    ) -> NodeId {
1862        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1863        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Function) {
1864            return id;
1865        }
1866        // Call-edge target stubs are not declarations -> is_definition = false.
1867        self.add_function_inner(qualified_name, span, is_async, is_unsafe, false)
1868    }
1869
1870    /// Ensure a method node exists, creating it if needed.
1871    ///
1872    /// Cross-kind reuse: if a node with the same canonical qualified name
1873    /// already exists as any call-compatible kind (Function, Macro, Constant,
1874    /// `LambdaTarget`), the existing node is returned as-is. See
1875    /// [`ensure_function`](Self::ensure_function) for the rationale.
1876    pub fn ensure_method(
1877        &mut self,
1878        qualified_name: &str,
1879        span: Option<Span>,
1880        is_async: bool,
1881        is_static: bool,
1882    ) -> NodeId {
1883        let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1884        if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Method) {
1885            return id;
1886        }
1887        // Call-edge target stubs are not declarations -> is_definition = false.
1888        self.add_method_inner(qualified_name, span, is_async, is_static, false)
1889    }
1890
1891    /// Get statistics about what's been staged.
1892    #[must_use]
1893    pub fn stats(&self) -> HelperStats {
1894        let staging_stats = self.staging.stats();
1895        HelperStats {
1896            strings_interned: self.string_cache.len(),
1897            nodes_created: self.node_cache.len(),
1898            nodes_staged: staging_stats.nodes_staged,
1899            edges_staged: staging_stats.edges_staged,
1900        }
1901    }
1902
1903    // -----------------------------------------------------------------------
1904    // C indirect-call precision staging (Phase A, U10).
1905    //
1906    // All accessors below route into the per-file `CIndirectStagingPayload`
1907    // owned by `StagingGraph`. Non-C plugins never call these — the parent
1908    // `Option` on `StagingGraph` stays `None`, so the per-file staging
1909    // buffer is unchanged for the other 36 plugins.
1910    //
1911    // The methods here are intentionally narrow: each one performs a single
1912    // push or a single setter. The C plugin's Phase 1 walkers compose them
1913    // (see `sqry-lang-c::relations::graph_builder`); U11's Phase 3 commit
1914    // consumes the payload via `staging.take_c_indirect()`.
1915    // -----------------------------------------------------------------------
1916
1917    /// Record `target_fn_name` as address-taken on the per-file C indirect
1918    /// staging payload (DESIGN §2.5 patterns).
1919    ///
1920    /// The name is interned through the helper's standard staging
1921    /// interner — DESIGN §2.5 specifies a per-file `Vec<StringId>`, so
1922    /// each push goes through `self.intern(...)` so the local → global
1923    /// `StringId` remap built by `StagingGraph::commit_strings` applies
1924    /// uniformly during Phase 3 commit. U11 resolves each global
1925    /// `StringId` to its canonical `NodeId` via the post-unification
1926    /// qualified-name index and applies the `NodeFlags::ADDRESS_TAKEN`
1927    /// bit. Duplicates within a file are tolerated — `mark_address_taken`
1928    /// is idempotent.
1929    pub fn mark_function_address_taken_by_name(&mut self, target_fn_name: &str) {
1930        let id = self.intern(target_fn_name);
1931        self.staging
1932            .c_indirect_mut()
1933            .pending_address_taken_names
1934            .push(id);
1935    }
1936
1937    /// Install the per-file [`LocalScopeIndex`] on the C indirect staging
1938    /// payload (DESIGN §4.1).
1939    ///
1940    /// Called from the top of the C plugin's `build_graph` after running
1941    /// the tree-sitter scope-arena builder. U11 transfers the index into
1942    /// `CIndirectSideTables::local_scope_indices` keyed by `FileId`.
1943    pub fn set_local_scope_index(&mut self, index: LocalScopeIndex) {
1944        self.staging.c_indirect_mut().local_scope_index = Some(index);
1945    }
1946
1947    /// Push a [`PendingIndirectCallsite`] onto the per-file C indirect
1948    /// staging payload (DESIGN §4.2).
1949    ///
1950    /// The caller is identified by its qualified name string; U11 resolves
1951    /// it to a `NodeId` after Phase 4c-prime cross-file unification. U12's
1952    /// resolver consumes the callsite list in `pass5b_c_indirect_resolve`.
1953    pub fn push_indirect_callsite(
1954        &mut self,
1955        caller_qualified_name: &str,
1956        use_span: (usize, usize),
1957        shape: IndirectShape,
1958        argument_count: u32,
1959        is_async: bool,
1960    ) {
1961        self.staging
1962            .c_indirect_mut()
1963            .pending_indirect_callsites
1964            .push(PendingIndirectCallsite {
1965                caller_qualified_name: caller_qualified_name.to_string(),
1966                use_span,
1967                shape,
1968                argument_count,
1969                is_async,
1970            });
1971    }
1972
1973    /// Push a [`PendingBinding`] onto the per-file C indirect staging
1974    /// payload (DESIGN §7.1).
1975    ///
1976    /// Designated initializer (`{ .field = fn }`) and positional
1977    /// initializer (`{ fn1, fn2 }`) sites are both routed through this
1978    /// helper, with the `site_kind` discriminator preserved for U12's
1979    /// resolver.
1980    pub fn push_binding(
1981        &mut self,
1982        struct_tag: &str,
1983        field_name: &str,
1984        instance_name: &str,
1985        target_fn_name: &str,
1986        site_kind: BindingSiteKind,
1987    ) {
1988        self.staging
1989            .c_indirect_mut()
1990            .pending_bindings
1991            .push(PendingBinding {
1992                struct_tag: struct_tag.to_string(),
1993                field_name: field_name.to_string(),
1994                instance_name: instance_name.to_string(),
1995                target_fn_name: target_fn_name.to_string(),
1996                site_kind,
1997            });
1998    }
1999
2000    /// Push a struct function-pointer field signature onto the per-file C
2001    /// indirect staging payload (DESIGN §3.2.2 / §3.7).
2002    ///
2003    /// The signature follows the DESIGN §3.1 canonical-string grammar.
2004    /// U11 interns each leg and inserts into
2005    /// `CIndirectSideTables::struct_field_fnptr`.
2006    pub fn push_struct_field_fnptr_signature(
2007        &mut self,
2008        struct_tag: &str,
2009        field_name: &str,
2010        signature: &str,
2011    ) {
2012        self.staging
2013            .c_indirect_mut()
2014            .pending_struct_field_signatures
2015            .push((
2016                struct_tag.to_string(),
2017                field_name.to_string(),
2018                signature.to_string(),
2019            ));
2020    }
2021
2022    /// Immutable accessor for the C indirect staging payload, if any.
2023    ///
2024    /// Exposed for tests and for the C plugin's own walkers that need to
2025    /// consult prior state (e.g. to suppress duplicate emissions within
2026    /// the same file). Returns `None` until at least one
2027    /// `mark_function_address_taken_by_name` / `set_local_scope_index` /
2028    /// `push_indirect_callsite` / `push_binding` /
2029    /// `push_struct_field_fnptr_signature` call has populated the payload.
2030    #[must_use]
2031    pub fn c_indirect(&self) -> Option<&CIndirectStagingPayload> {
2032        self.staging.c_indirect()
2033    }
2034}
2035
2036fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
2037    if original.contains('/') {
2038        return original.to_string();
2039    }
2040
2041    canonical
2042        .rsplit("::")
2043        .next()
2044        .map_or_else(|| original.to_string(), ToString::to_string)
2045}
2046
2047/// Statistics from `GraphBuildHelper` operations.
2048#[derive(Debug, Clone, Default)]
2049pub struct HelperStats {
2050    /// Number of unique strings interned.
2051    pub strings_interned: usize,
2052    /// Number of unique nodes created.
2053    pub nodes_created: usize,
2054    /// Total nodes staged (from `StagingGraph`).
2055    pub nodes_staged: usize,
2056    /// Total edges staged (from `StagingGraph`).
2057    pub edges_staged: usize,
2058}
2059
2060#[cfg(test)]
2061mod tests {
2062    use super::*;
2063    use crate::graph::node::Position;
2064    use crate::graph::unified::build::staging::StagingOp;
2065    use std::path::PathBuf;
2066
2067    #[test]
2068    fn test_helper_add_function() {
2069        let mut staging = StagingGraph::new();
2070        let file = PathBuf::from("test.rs");
2071        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2072
2073        let node_id = helper.add_function("main", None, false, false);
2074        assert!(!node_id.is_invalid());
2075        assert_eq!(helper.stats().nodes_created, 1);
2076    }
2077
2078    #[test]
2079    fn test_helper_deduplication() {
2080        let mut staging = StagingGraph::new();
2081        let file = PathBuf::from("test.rs");
2082        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2083
2084        let id1 = helper.add_function("main", None, false, false);
2085        let id2 = helper.add_function("main", None, false, false);
2086
2087        assert_eq!(id1, id2, "Same function should return same NodeId");
2088        assert_eq!(
2089            helper.stats().nodes_created,
2090            1,
2091            "Should only create one node"
2092        );
2093    }
2094
2095    #[test]
2096    fn test_helper_string_interning() {
2097        let mut staging = StagingGraph::new();
2098        let file = PathBuf::from("test.rs");
2099        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2100
2101        let s1 = helper.intern("hello");
2102        let s2 = helper.intern("world");
2103        let s3 = helper.intern("hello"); // Duplicate
2104
2105        assert_ne!(s1, s2, "Different strings should have different IDs");
2106        assert_eq!(s1, s3, "Same string should return same ID");
2107        assert_eq!(helper.stats().strings_interned, 2);
2108    }
2109
2110    #[test]
2111    fn test_helper_add_call_edge() {
2112        let mut staging = StagingGraph::new();
2113        let file = PathBuf::from("test.rs");
2114        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2115
2116        let main_id = helper.add_function("main", None, false, false);
2117        let helper_id = helper.add_function("helper", None, false, false);
2118
2119        helper.add_call_edge(main_id, helper_id);
2120
2121        assert_eq!(helper.stats().edges_staged, 1);
2122        let edge_kind = staging.operations().iter().find_map(|op| {
2123            if let StagingOp::AddEdge { kind, .. } = op {
2124                Some(kind)
2125            } else {
2126                None
2127            }
2128        });
2129        match edge_kind {
2130            Some(EdgeKind::Calls {
2131                argument_count,
2132                is_async,
2133                ..
2134            }) => {
2135                assert_eq!(*argument_count, 255);
2136                assert!(!*is_async);
2137            }
2138            _ => panic!("Expected Calls edge"),
2139        }
2140    }
2141
2142    #[test]
2143    fn test_helper_multiple_node_kinds() {
2144        let mut staging = StagingGraph::new();
2145        let file = PathBuf::from("test.py");
2146        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
2147
2148        let _class_id = helper.add_class("MyClass", None);
2149        let _method_id = helper.add_method("MyClass.my_method", None, false, false);
2150        let _func_id = helper.add_function("standalone_func", None, true, false);
2151
2152        assert_eq!(helper.stats().nodes_created, 3);
2153    }
2154
2155    #[test]
2156    fn test_helper_canonicalizes_language_native_qualified_names() {
2157        let mut staging = StagingGraph::new();
2158        let file = PathBuf::from("test.py");
2159        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
2160
2161        let _method_id = helper.add_method("pkg.module.run", None, false, false);
2162
2163        let add_node_op = staging
2164            .operations()
2165            .iter()
2166            .find(|op| matches!(op, StagingOp::AddNode { .. }))
2167            .expect("Expected AddNode operation");
2168
2169        if let StagingOp::AddNode { entry, .. } = add_node_op {
2170            assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
2171            assert_eq!(
2172                staging.resolve_node_name(entry),
2173                Some("pkg::module::run"),
2174                "expected GraphBuildHelper to canonicalize Python dotted qualified names"
2175            );
2176        }
2177    }
2178
2179    #[test]
2180    fn test_helper_preserves_path_qualified_names() {
2181        let mut staging = StagingGraph::new();
2182        let file = PathBuf::from("test.js");
2183        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2184
2185        let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
2186
2187        let add_node_op = staging
2188            .operations()
2189            .iter()
2190            .find(|op| matches!(op, StagingOp::AddNode { .. }))
2191            .expect("Expected AddNode operation");
2192
2193        if let StagingOp::AddNode { entry, .. } = add_node_op {
2194            assert_eq!(
2195                staging.resolve_local_string(entry.name),
2196                Some("frontend/api.js::fetchUsers")
2197            );
2198            assert_eq!(
2199                staging.resolve_node_name(entry),
2200                Some("frontend/api.js::fetchUsers"),
2201                "expected path-qualified names to remain unchanged"
2202            );
2203        }
2204    }
2205
2206    #[test]
2207    fn test_helper_verbatim_import_preserves_resource_name() {
2208        let mut staging = StagingGraph::new();
2209        let file = PathBuf::from("index.html");
2210        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
2211
2212        let _import_id = helper.add_verbatim_import("styles.css", None);
2213
2214        let add_node_op = staging
2215            .operations()
2216            .iter()
2217            .find(|op| matches!(op, StagingOp::AddNode { .. }))
2218            .expect("Expected AddNode operation");
2219
2220        if let StagingOp::AddNode { entry, .. } = add_node_op {
2221            assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
2222            assert_eq!(entry.qualified_name, None);
2223            assert_eq!(
2224                staging.resolve_node_name(entry),
2225                Some("styles.css"),
2226                "expected verbatim resource imports to preserve their literal identity"
2227            );
2228        }
2229    }
2230
2231    #[test]
2232    fn test_helper_verbatim_variable_preserves_resource_name() {
2233        let mut staging = StagingGraph::new();
2234        let file = PathBuf::from("index.html");
2235        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
2236
2237        let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
2238
2239        let add_node_op = staging
2240            .operations()
2241            .iter()
2242            .find(|op| matches!(op, StagingOp::AddNode { .. }))
2243            .expect("Expected AddNode operation");
2244
2245        if let StagingOp::AddNode { entry, .. } = add_node_op {
2246            assert_eq!(
2247                staging.resolve_local_string(entry.name),
2248                Some("/assets/logo.icon.png")
2249            );
2250            assert_eq!(entry.qualified_name, None);
2251            assert_eq!(
2252                staging.resolve_node_name(entry),
2253                Some("/assets/logo.icon.png"),
2254                "expected verbatim resource variables to preserve their literal identity"
2255            );
2256        }
2257    }
2258
2259    #[test]
2260    fn test_helper_ensure_function() {
2261        let mut staging = StagingGraph::new();
2262        let file = PathBuf::from("test.rs");
2263        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2264
2265        let id1 = helper.ensure_function("foo", None, false, false);
2266        let id2 = helper.ensure_function("foo", None, true, false); // Different attrs, same name
2267
2268        assert_eq!(id1, id2, "ensure_function should be idempotent by name");
2269    }
2270
2271    #[test]
2272    fn test_helper_with_span() {
2273        let mut staging = StagingGraph::new();
2274        let file = PathBuf::from("test.rs");
2275        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2276
2277        let span = Span {
2278            start: Position {
2279                line: 10,
2280                column: 0,
2281            },
2282            end: Position {
2283                line: 15,
2284                column: 1,
2285            },
2286        };
2287
2288        let node_id = helper.add_function("main", Some(span), false, false);
2289        assert!(!node_id.is_invalid());
2290    }
2291
2292    #[test]
2293    fn test_helper_add_call_edge_full() {
2294        let mut staging = StagingGraph::new();
2295        let file = PathBuf::from("test.rs");
2296        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2297
2298        let caller_id = helper.add_function("caller", None, false, false);
2299        let callee_id = helper.add_function("callee", None, false, false);
2300
2301        // Add a call with specific metadata
2302        helper.add_call_edge_full(caller_id, callee_id, 3, true);
2303
2304        assert_eq!(helper.stats().edges_staged, 1);
2305
2306        // Verify the edge has correct metadata
2307        let edges = staging.operations();
2308        let call_edge = edges.iter().find(|op| {
2309            matches!(
2310                op,
2311                StagingOp::AddEdge {
2312                    kind: EdgeKind::Calls { .. },
2313                    ..
2314                }
2315            )
2316        });
2317
2318        assert!(call_edge.is_some());
2319        if let StagingOp::AddEdge {
2320            kind:
2321                EdgeKind::Calls {
2322                    argument_count,
2323                    is_async,
2324                    ..
2325                },
2326            ..
2327        } = call_edge.unwrap()
2328        {
2329            assert_eq!(*argument_count, 3);
2330            assert!(*is_async);
2331        }
2332    }
2333
2334    #[test]
2335    fn test_helper_add_import_edge_full() {
2336        let mut staging = StagingGraph::new();
2337        let file = PathBuf::from("test.js");
2338        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2339
2340        let module_id = helper.add_module("app", None);
2341        let imported_id = helper.add_function("utils", None, false, false);
2342
2343        // Import with alias
2344        helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
2345
2346        assert_eq!(helper.stats().edges_staged, 1);
2347
2348        // Verify the edge has correct metadata
2349        let edges = staging.operations();
2350        let import_edge = edges.iter().find(|op| {
2351            matches!(
2352                op,
2353                StagingOp::AddEdge {
2354                    kind: EdgeKind::Imports { .. },
2355                    ..
2356                }
2357            )
2358        });
2359
2360        assert!(import_edge.is_some());
2361        if let StagingOp::AddEdge {
2362            kind: EdgeKind::Imports { alias, is_wildcard },
2363            ..
2364        } = import_edge.unwrap()
2365        {
2366            assert!(alias.is_some(), "Alias should be present");
2367            assert!(!*is_wildcard);
2368        }
2369    }
2370
2371    #[test]
2372    fn test_helper_add_import_edge_wildcard() {
2373        let mut staging = StagingGraph::new();
2374        let file = PathBuf::from("test.js");
2375        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2376
2377        let module_id = helper.add_module("app", None);
2378        let imported_id = helper.add_module("lodash", None);
2379
2380        // Wildcard import: import * from "lodash"
2381        helper.add_import_edge_full(module_id, imported_id, None, true);
2382
2383        let edges = staging.operations();
2384        let import_edge = edges.iter().find(|op| {
2385            matches!(
2386                op,
2387                StagingOp::AddEdge {
2388                    kind: EdgeKind::Imports { .. },
2389                    ..
2390                }
2391            )
2392        });
2393
2394        if let StagingOp::AddEdge {
2395            kind: EdgeKind::Imports { alias, is_wildcard },
2396            ..
2397        } = import_edge.unwrap()
2398        {
2399            assert!(alias.is_none());
2400            assert!(*is_wildcard);
2401        }
2402    }
2403
2404    #[test]
2405    fn test_helper_add_export_edge_full() {
2406        let mut staging = StagingGraph::new();
2407        let file = PathBuf::from("test.js");
2408        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2409
2410        let module_id = helper.add_module("app", None);
2411        let component_id = helper.add_class("MyComponent", None);
2412
2413        // Default export
2414        helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
2415
2416        assert_eq!(helper.stats().edges_staged, 1);
2417
2418        let edges = staging.operations();
2419        let export_edge = edges.iter().find(|op| {
2420            matches!(
2421                op,
2422                StagingOp::AddEdge {
2423                    kind: EdgeKind::Exports { .. },
2424                    ..
2425                }
2426            )
2427        });
2428
2429        assert!(export_edge.is_some());
2430        if let StagingOp::AddEdge {
2431            kind: EdgeKind::Exports { kind, alias },
2432            ..
2433        } = export_edge.unwrap()
2434        {
2435            assert_eq!(*kind, ExportKind::Default);
2436            assert!(alias.is_none());
2437        }
2438    }
2439
2440    #[test]
2441    fn test_helper_add_export_edge_with_alias() {
2442        let mut staging = StagingGraph::new();
2443        let file = PathBuf::from("test.js");
2444        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2445
2446        let module_id = helper.add_module("app", None);
2447        let helper_fn_id = helper.add_function("internalHelper", None, false, false);
2448
2449        // export { internalHelper as helper }
2450        helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
2451
2452        let edges = staging.operations();
2453        let export_edge = edges.iter().find(|op| {
2454            matches!(
2455                op,
2456                StagingOp::AddEdge {
2457                    kind: EdgeKind::Exports { .. },
2458                    ..
2459                }
2460            )
2461        });
2462
2463        if let StagingOp::AddEdge {
2464            kind: EdgeKind::Exports { kind, alias },
2465            ..
2466        } = export_edge.unwrap()
2467        {
2468            assert_eq!(*kind, ExportKind::Direct);
2469            assert!(alias.is_some(), "Alias should be present");
2470        }
2471    }
2472
2473    #[test]
2474    fn test_helper_add_export_edge_reexport() {
2475        let mut staging = StagingGraph::new();
2476        let file = PathBuf::from("index.js");
2477        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
2478
2479        let module_id = helper.add_module("index", None);
2480        let utils_id = helper.add_module("utils", None);
2481
2482        // export * as utils from "./utils"
2483        helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
2484
2485        let edges = staging.operations();
2486        let export_edge = edges.iter().find(|op| {
2487            matches!(
2488                op,
2489                StagingOp::AddEdge {
2490                    kind: EdgeKind::Exports { .. },
2491                    ..
2492                }
2493            )
2494        });
2495
2496        if let StagingOp::AddEdge {
2497            kind: EdgeKind::Exports { kind, alias },
2498            ..
2499        } = export_edge.unwrap()
2500        {
2501            assert_eq!(*kind, ExportKind::Namespace);
2502            assert!(alias.is_some());
2503        }
2504    }
2505
2506    #[test]
2507    fn test_helper_add_call_edge_full_with_span() {
2508        let mut staging = StagingGraph::new();
2509        let file = PathBuf::from("test.rs");
2510        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2511
2512        let caller_id = helper.add_function("caller", None, false, false);
2513        let callee_id = helper.add_function("callee", None, false, false);
2514
2515        let span = Span {
2516            start: Position { line: 5, column: 4 },
2517            end: Position {
2518                line: 5,
2519                column: 20,
2520            },
2521        };
2522
2523        helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
2524
2525        let edges = staging.operations();
2526        let call_edge = edges.iter().find(|op| {
2527            matches!(
2528                op,
2529                StagingOp::AddEdge {
2530                    kind: EdgeKind::Calls { .. },
2531                    ..
2532                }
2533            )
2534        });
2535
2536        if let StagingOp::AddEdge {
2537            kind:
2538                EdgeKind::Calls {
2539                    argument_count,
2540                    is_async,
2541                    ..
2542                },
2543            spans: edge_spans,
2544            ..
2545        } = call_edge.unwrap()
2546        {
2547            assert_eq!(*argument_count, 2);
2548            assert!(!*is_async);
2549            assert!(!edge_spans.is_empty());
2550        }
2551    }
2552
2553    #[test]
2554    fn test_helper_add_function_with_async_attribute() {
2555        let mut staging = StagingGraph::new();
2556        let file = PathBuf::from("test.kt");
2557        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
2558
2559        // Add an async (suspend) function
2560        let _func_id = helper.add_function("fetchData", None, true, false);
2561
2562        // Verify the staged node has is_async = true
2563        let ops = staging.operations();
2564        let add_node_op = ops
2565            .iter()
2566            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2567
2568        assert!(add_node_op.is_some(), "Expected AddNode operation");
2569        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2570            assert!(
2571                entry.is_async,
2572                "Expected is_async=true for suspend function, got is_async=false"
2573            );
2574        }
2575    }
2576
2577    #[test]
2578    fn test_helper_add_method_with_static_attribute() {
2579        let mut staging = StagingGraph::new();
2580        let file = PathBuf::from("test.java");
2581        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
2582
2583        // Add a static method
2584        let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
2585
2586        // Verify the staged node has is_static = true
2587        let ops = staging.operations();
2588        let add_node_op = ops
2589            .iter()
2590            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2591
2592        assert!(add_node_op.is_some(), "Expected AddNode operation");
2593        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2594            assert!(
2595                entry.is_static,
2596                "Expected is_static=true for static method, got is_static=false"
2597            );
2598        }
2599    }
2600
2601    #[test]
2602    fn test_helper_add_function_without_attributes() {
2603        let mut staging = StagingGraph::new();
2604        let file = PathBuf::from("test.rs");
2605        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2606
2607        // Add a regular function (not async, not unsafe)
2608        let _func_id = helper.add_function("regular_function", None, false, false);
2609
2610        // Verify the staged node has is_async = false
2611        let ops = staging.operations();
2612        let add_node_op = ops
2613            .iter()
2614            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2615
2616        assert!(add_node_op.is_some(), "Expected AddNode operation");
2617        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2618            assert!(
2619                !entry.is_async,
2620                "Expected is_async=false for regular function"
2621            );
2622            assert!(
2623                !entry.is_static,
2624                "Expected is_static=false for regular function"
2625            );
2626        }
2627    }
2628
2629    #[test]
2630    fn test_helper_add_method_with_both_attributes() {
2631        let mut staging = StagingGraph::new();
2632        let file = PathBuf::from("test.kt");
2633        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
2634
2635        // Add an async static method
2636        let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
2637
2638        // Verify the staged node has both flags set
2639        let ops = staging.operations();
2640        let add_node_op = ops
2641            .iter()
2642            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2643
2644        assert!(add_node_op.is_some(), "Expected AddNode operation");
2645        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2646            assert!(entry.is_async, "Expected is_async=true for async method");
2647            assert!(entry.is_static, "Expected is_static=true for static method");
2648        }
2649    }
2650
2651    #[test]
2652    fn test_helper_add_function_with_unsafe_attribute() {
2653        let mut staging = StagingGraph::new();
2654        let file = PathBuf::from("test.rs");
2655        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2656
2657        // Add an unsafe function
2658        let _func_id = helper.add_function("unsafe_function", None, false, true);
2659
2660        // Verify the staged node has is_unsafe = true
2661        let ops = staging.operations();
2662        let add_node_op = ops
2663            .iter()
2664            .find(|op| matches!(op, StagingOp::AddNode { .. }));
2665
2666        assert!(add_node_op.is_some(), "Expected AddNode operation");
2667        if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2668            assert!(
2669                entry.is_unsafe,
2670                "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
2671                entry.is_unsafe
2672            );
2673        }
2674    }
2675
2676    // ========================================================================
2677    // Cross-kind reuse tests (Method/Function NodeKind mismatch fix)
2678    // ========================================================================
2679
2680    #[test]
2681    fn test_ensure_function_reuses_existing_method_node() {
2682        let mut staging = StagingGraph::new();
2683        let file = PathBuf::from("test.ts");
2684        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2685
2686        let span = Span::new(
2687            Position { line: 5, column: 4 },
2688            Position {
2689                line: 10,
2690                column: 5,
2691            },
2692        );
2693
2694        // Stage 1: create a Method node with proper span
2695        let method_id = helper.add_method("MyClass.doWork", Some(span), true, false);
2696
2697        // Stage 2: ensure_function for the same qualified name
2698        let reused_id = helper.ensure_function("MyClass.doWork", None, true, false);
2699
2700        assert_eq!(
2701            method_id, reused_id,
2702            "ensure_function should reuse the existing Method node"
2703        );
2704        assert_eq!(
2705            helper.stats().nodes_created,
2706            1,
2707            "Only the Method node should exist"
2708        );
2709    }
2710
2711    #[test]
2712    fn test_ensure_method_reuses_existing_function_node() {
2713        let mut staging = StagingGraph::new();
2714        let file = PathBuf::from("test.ts");
2715        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2716
2717        let func_id = helper.add_function("standalone", None, false, false);
2718        let reused_id = helper.ensure_method("standalone", None, false, false);
2719
2720        assert_eq!(
2721            func_id, reused_id,
2722            "ensure_method should reuse the existing function node"
2723        );
2724        assert_eq!(helper.stats().nodes_created, 1);
2725    }
2726
2727    #[test]
2728    fn test_ensure_function_creates_new_when_no_method_exists() {
2729        let mut staging = StagingGraph::new();
2730        let file = PathBuf::from("test.ts");
2731        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2732
2733        let func_id = helper.ensure_function("topLevel", None, false, false);
2734        assert!(!func_id.is_invalid());
2735        assert_eq!(helper.stats().nodes_created, 1);
2736
2737        let func_id2 = helper.ensure_function("topLevel", None, false, false);
2738        assert_eq!(func_id, func_id2);
2739        assert_eq!(helper.stats().nodes_created, 1);
2740    }
2741
2742    #[test]
2743    fn test_no_method_function_duplicate_after_cross_kind_reuse() {
2744        let mut staging = StagingGraph::new();
2745        let file = PathBuf::from("browser-manager.ts");
2746        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2747
2748        let span_a = Span::new(
2749            Position { line: 3, column: 4 },
2750            Position { line: 8, column: 5 },
2751        );
2752        let span_b = Span::new(
2753            Position {
2754                line: 10,
2755                column: 4,
2756            },
2757            Position {
2758                line: 15,
2759                column: 5,
2760            },
2761        );
2762
2763        // Stage 1: create Method nodes
2764        let _method_a = helper.add_method("BrowserManager.newTab", Some(span_a), true, false);
2765        let _method_b = helper.add_method("BrowserManager.restoreState", Some(span_b), true, false);
2766
2767        // Stage 2: ensure_function for call-edge construction
2768        let _caller_a = helper.ensure_function("BrowserManager.newTab", None, true, false);
2769        let _caller_b = helper.ensure_function("BrowserManager.restoreState", None, true, false);
2770
2771        // Verify: no same-name Method/NodeKind::Function duplicates
2772        let ops = staging.operations();
2773        let mut method_names = std::collections::HashSet::new();
2774        let mut function_names = std::collections::HashSet::new();
2775
2776        for op in ops {
2777            if let StagingOp::AddNode { entry, .. } = op {
2778                if entry.kind == NodeKind::Method {
2779                    method_names.insert(entry.name);
2780                } else if entry.kind == NodeKind::Function {
2781                    function_names.insert(entry.name);
2782                }
2783            }
2784        }
2785
2786        let overlap: Vec<_> = method_names.intersection(&function_names).collect();
2787        assert!(
2788            overlap.is_empty(),
2789            "Found names that are both Method and Function: {overlap:?}"
2790        );
2791    }
2792
2793    // ========================================================================
2794    // Generalized cross-kind reuse tests (HU01: CALL_COMPATIBLE_KINDS)
2795    // ========================================================================
2796
2797    #[test]
2798    fn test_ensure_function_reuses_existing_macro_node() {
2799        let mut staging = StagingGraph::new();
2800        let file = PathBuf::from("test.c");
2801        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2802
2803        let span = Span::new(
2804            Position { line: 1, column: 0 },
2805            Position {
2806                line: 1,
2807                column: 40,
2808            },
2809        );
2810
2811        // Stage 1: create a Macro node (e.g., list_for_each_entry in C kernel code)
2812        let macro_id = helper.add_node("list_for_each_entry", Some(span), NodeKind::Macro);
2813
2814        // Stage 2: ensure_function for the same name (call-edge construction)
2815        let reused_id = helper.ensure_function("list_for_each_entry", None, false, false);
2816
2817        assert_eq!(
2818            macro_id, reused_id,
2819            "ensure_function should reuse the existing Macro node"
2820        );
2821        assert_eq!(helper.stats().nodes_created, 1);
2822    }
2823
2824    #[test]
2825    fn test_ensure_function_reuses_existing_constant_node() {
2826        let mut staging = StagingGraph::new();
2827        let file = PathBuf::from("test.c");
2828        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2829
2830        let span = Span::new(
2831            Position { line: 3, column: 0 },
2832            Position {
2833                line: 3,
2834                column: 30,
2835            },
2836        );
2837
2838        // A function pointer constant in C
2839        let const_id = helper.add_constant("handler_fn", Some(span));
2840
2841        let reused_id = helper.ensure_function("handler_fn", None, false, false);
2842
2843        assert_eq!(
2844            const_id, reused_id,
2845            "ensure_function should reuse the existing Constant node"
2846        );
2847        assert_eq!(helper.stats().nodes_created, 1);
2848    }
2849
2850    #[test]
2851    fn test_ensure_method_reuses_existing_lambda_target_node() {
2852        let mut staging = StagingGraph::new();
2853        let file = PathBuf::from("test.java");
2854        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
2855
2856        let span = Span::new(
2857            Position { line: 7, column: 8 },
2858            Position {
2859                line: 10,
2860                column: 9,
2861            },
2862        );
2863
2864        let lambda_id = helper.add_node("Comparator.compare", Some(span), NodeKind::LambdaTarget);
2865
2866        let reused_id = helper.ensure_method("Comparator.compare", None, false, false);
2867
2868        assert_eq!(
2869            lambda_id, reused_id,
2870            "ensure_method should reuse the existing LambdaTarget node"
2871        );
2872        assert_eq!(helper.stats().nodes_created, 1);
2873    }
2874
2875    #[test]
2876    fn test_cross_kind_reuse_does_not_merge_incompatible_kinds() {
2877        let mut staging = StagingGraph::new();
2878        let file = PathBuf::from("test.css");
2879        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Css);
2880
2881        // Create a StyleRule node — NOT a call-compatible kind
2882        let style_id = helper.add_node_verbatim(
2883            ".container",
2884            None,
2885            NodeKind::StyleRule,
2886            &[],
2887            None,
2888            None,
2889            false,
2890        );
2891
2892        // ensure_function with the same name should NOT reuse the StyleRule
2893        let func_id = helper.ensure_function(".container", None, false, false);
2894
2895        assert_ne!(
2896            style_id, func_id,
2897            "ensure_function must NOT merge into a StyleRule"
2898        );
2899        assert_eq!(helper.stats().nodes_created, 2);
2900    }
2901
2902    // ========================================================================
2903    // Stub-first order tests (Codex review M1: ensure_* before add_*)
2904    // Proves cross-kind reuse works when the STUB is created first and
2905    // the real declaration arrives later — the actual line-zero failure mode.
2906    // ========================================================================
2907
2908    #[test]
2909    fn test_stub_first_ensure_function_then_add_method_reuses() {
2910        let mut staging = StagingGraph::new();
2911        let file = PathBuf::from("test.ts");
2912        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2913
2914        // Stage 2 runs first (call-edge construction creates a Function stub)
2915        let stub_id = helper.ensure_function("Widget.render", None, false, false);
2916
2917        // Stage 1 runs later (declaration extraction creates Method with real span)
2918        let span = Span::new(
2919            Position {
2920                line: 10,
2921                column: 4,
2922            },
2923            Position {
2924                line: 20,
2925                column: 5,
2926            },
2927        );
2928        let decl_id = helper.add_method("Widget.render", Some(span), false, false);
2929
2930        // The two calls should produce DIFFERENT NodeIds because add_method
2931        // uses its own (name, Method) cache key while ensure_function created
2932        // (name, Function). This is the scenario Phase 4c-prime unifies later.
2933        // What matters here: NO PANIC, and both IDs are valid.
2934        assert!(!stub_id.is_invalid());
2935        assert!(!decl_id.is_invalid());
2936        // If they are different, Phase 4c-prime handles the merge.
2937        // If add_node_internal deduped them (same canonical), that's also fine.
2938    }
2939
2940    #[test]
2941    fn test_stub_first_ensure_method_then_add_function_reuses() {
2942        let mut staging = StagingGraph::new();
2943        let file = PathBuf::from("test.py");
2944        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
2945
2946        // Stub created first
2947        let stub_id = helper.ensure_method("process_data", None, false, false);
2948
2949        // Real declaration arrives
2950        let span = Span::new(
2951            Position { line: 5, column: 0 },
2952            Position {
2953                line: 15,
2954                column: 0,
2955            },
2956        );
2957        let decl_id = helper.add_function("process_data", Some(span), false, false);
2958
2959        assert!(!stub_id.is_invalid());
2960        assert!(!decl_id.is_invalid());
2961    }
2962
2963    #[test]
2964    fn test_ensure_callee_then_add_function_same_name_no_panic() {
2965        let mut staging = StagingGraph::new();
2966        let file = PathBuf::from("test.c");
2967        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2968
2969        let call_span = Span::new(
2970            Position {
2971                line: 50,
2972                column: 4,
2973            },
2974            Position {
2975                line: 50,
2976                column: 20,
2977            },
2978        );
2979        let callee_id = helper.ensure_callee("kfree", call_span, CalleeKindHint::Function);
2980
2981        let def_span = Span::new(
2982            Position { line: 1, column: 0 },
2983            Position {
2984                line: 10,
2985                column: 1,
2986            },
2987        );
2988        let def_id = helper.add_function("kfree", Some(def_span), false, false);
2989
2990        // ensure_callee already created a Function node for "kfree", so
2991        // add_function should return the same NodeId (same cache key).
2992        assert_eq!(
2993            callee_id, def_id,
2994            "add_function should reuse the node created by ensure_callee"
2995        );
2996        assert_eq!(helper.stats().nodes_created, 1);
2997    }
2998
2999    // ========================================================================
3000    // ensure_callee tests (HU02)
3001    // ========================================================================
3002
3003    #[test]
3004    fn test_ensure_callee_function_hint_creates_with_span() {
3005        let mut staging = StagingGraph::new();
3006        let file = PathBuf::from("test.rs");
3007        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
3008
3009        let call_span = Span::new(
3010            Position {
3011                line: 20,
3012                column: 4,
3013            },
3014            Position {
3015                line: 20,
3016                column: 30,
3017            },
3018        );
3019
3020        let id = helper.ensure_callee("target_fn", call_span, CalleeKindHint::Function);
3021        assert!(!id.is_invalid());
3022
3023        // The created node should have start_line > 0 (from the call-site span)
3024        let ops = staging.operations();
3025        let node_op = ops
3026            .iter()
3027            .find(|op| matches!(op, StagingOp::AddNode { .. }));
3028        if let Some(StagingOp::AddNode { entry, .. }) = node_op {
3029            assert!(
3030                entry.start_line > 0,
3031                "ensure_callee must produce nodes with line > 0"
3032            );
3033        }
3034    }
3035
3036    #[test]
3037    fn test_ensure_callee_macro_hint_reuses_existing_macro() {
3038        let mut staging = StagingGraph::new();
3039        let file = PathBuf::from("test.c");
3040        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
3041
3042        let def_span = Span::new(
3043            Position { line: 5, column: 0 },
3044            Position {
3045                line: 5,
3046                column: 40,
3047            },
3048        );
3049        let call_span = Span::new(
3050            Position {
3051                line: 99,
3052                column: 4,
3053            },
3054            Position {
3055                line: 99,
3056                column: 30,
3057            },
3058        );
3059
3060        let macro_id = helper.add_node("IS_ERR", Some(def_span), NodeKind::Macro);
3061        let reused_id = helper.ensure_callee("IS_ERR", call_span, CalleeKindHint::Macro);
3062
3063        assert_eq!(
3064            macro_id, reused_id,
3065            "ensure_callee should reuse existing Macro node"
3066        );
3067        assert_eq!(helper.stats().nodes_created, 1);
3068    }
3069
3070    #[test]
3071    fn test_ensure_callee_idempotent_returns_first_spans_node() {
3072        let mut staging = StagingGraph::new();
3073        let file = PathBuf::from("test.rs");
3074        let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
3075
3076        let span1 = Span::new(
3077            Position {
3078                line: 10,
3079                column: 0,
3080            },
3081            Position {
3082                line: 10,
3083                column: 20,
3084            },
3085        );
3086        let span2 = Span::new(
3087            Position {
3088                line: 50,
3089                column: 0,
3090            },
3091            Position {
3092                line: 50,
3093                column: 20,
3094            },
3095        );
3096
3097        let id1 = helper.ensure_callee("func", span1, CalleeKindHint::Function);
3098        let id2 = helper.ensure_callee("func", span2, CalleeKindHint::Function);
3099
3100        assert_eq!(
3101            id1, id2,
3102            "Two ensure_callee calls for the same name return the same NodeId"
3103        );
3104    }
3105
3106    #[test]
3107    fn test_call_compatible_kinds_dry_no_body_changes_needed() {
3108        // Compile-time proof: adding a variant to CALL_COMPATIBLE_KINDS does
3109        // NOT require touching ensure_function or ensure_method bodies. Both
3110        // delegate to reuse_across_call_compatible_kinds which iterates the
3111        // const slice. This test simply asserts the slice contains the expected
3112        // entries to catch accidental removals.
3113        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Function));
3114        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Method));
3115        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Macro));
3116        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Constant));
3117        assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::LambdaTarget));
3118        assert_eq!(CALL_COMPATIBLE_KINDS.len(), 5);
3119    }
3120}