Skip to main content

sqry_core/graph/unified/concurrent/
graph.rs

1//! `CodeGraph` and `ConcurrentCodeGraph` implementations.
2//!
3//! This module provides the core graph types with thread-safe access:
4//!
5//! - [`CodeGraph`]: Arc-wrapped internals for O(1) `CoW` snapshots
6//! - [`ConcurrentCodeGraph`]: `RwLock` wrapper with epoch versioning
7//! - [`GraphSnapshot`]: Immutable snapshot for long-running queries
8
9use std::collections::HashMap;
10use std::fmt;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
15
16use crate::confidence::ConfidenceMetadata;
17use crate::graph::unified::edge::bidirectional::BidirectionalEdgeStore;
18use crate::graph::unified::memory::{GraphMemorySize, HASHMAP_ENTRY_OVERHEAD};
19use crate::graph::unified::storage::arena::NodeArena;
20use crate::graph::unified::storage::edge_provenance::{EdgeProvenance, EdgeProvenanceStore};
21use crate::graph::unified::storage::indices::AuxiliaryIndices;
22use crate::graph::unified::storage::interner::StringInterner;
23use crate::graph::unified::storage::metadata::NodeMetadataStore;
24use crate::graph::unified::storage::node_provenance::{NodeProvenance, NodeProvenanceStore};
25use crate::graph::unified::storage::registry::{FileProvenanceView, FileRegistry};
26use crate::graph::unified::string::id::StringId;
27
28/// Core graph with Arc-wrapped internals for O(1) `CoW` snapshots.
29///
30/// `CodeGraph` uses `Arc` for all internal components, enabling:
31/// - O(1) snapshot creation via Arc cloning
32/// - Copy-on-write semantics via `Arc::make_mut`
33/// - Memory-efficient sharing between snapshots
34///
35/// # Design
36///
37/// The Arc wrapping enables the MVCC pattern:
38/// - Readers see a consistent snapshot at the time they acquired access
39/// - Writers use `Arc::make_mut` to get exclusive copies only when mutating
40/// - Multiple snapshots can coexist without copying data
41///
42/// # Performance
43///
44/// - Snapshot creation: O(5) Arc clones ≈ <1μs
45/// - Read access: Direct Arc dereference, no locking
46/// - Write access: `Arc::make_mut` clones only if refcount > 1
47#[derive(Clone)]
48pub struct CodeGraph {
49    /// Node storage with generational indices.
50    nodes: Arc<NodeArena>,
51    /// Bidirectional edge storage (forward + reverse).
52    edges: Arc<BidirectionalEdgeStore>,
53    /// String interner for symbol names.
54    strings: Arc<StringInterner>,
55    /// File registry for path deduplication.
56    files: Arc<FileRegistry>,
57    /// Auxiliary indices for fast lookup.
58    indices: Arc<AuxiliaryIndices>,
59    /// Sparse macro boundary metadata (keyed by full `NodeId`).
60    macro_metadata: Arc<NodeMetadataStore>,
61    /// Dense node provenance (Phase 1 fact layer).
62    node_provenance: Arc<NodeProvenanceStore>,
63    /// Dense edge provenance (Phase 1 fact layer).
64    edge_provenance: Arc<EdgeProvenanceStore>,
65    /// Monotonic fact-layer epoch (0 until set by the V8 persistence path).
66    fact_epoch: u64,
67    /// Epoch for version tracking.
68    epoch: u64,
69    /// Per-language confidence metadata collected during build.
70    /// Maps language name (e.g., "rust") to aggregated confidence.
71    confidence: HashMap<String, ConfidenceMetadata>,
72}
73
74impl CodeGraph {
75    /// Creates a new empty `CodeGraph`.
76    ///
77    /// # Example
78    ///
79    /// ```rust
80    /// use sqry_core::graph::unified::concurrent::CodeGraph;
81    ///
82    /// let graph = CodeGraph::new();
83    /// assert_eq!(graph.epoch(), 0);
84    /// ```
85    #[must_use]
86    pub fn new() -> Self {
87        Self {
88            nodes: Arc::new(NodeArena::new()),
89            edges: Arc::new(BidirectionalEdgeStore::new()),
90            strings: Arc::new(StringInterner::new()),
91            files: Arc::new(FileRegistry::new()),
92            indices: Arc::new(AuxiliaryIndices::new()),
93            macro_metadata: Arc::new(NodeMetadataStore::new()),
94            node_provenance: Arc::new(NodeProvenanceStore::new()),
95            edge_provenance: Arc::new(EdgeProvenanceStore::new()),
96            fact_epoch: 0,
97            epoch: 0,
98            confidence: HashMap::new(),
99        }
100    }
101
102    /// Creates a `CodeGraph` from existing components.
103    ///
104    /// This is useful when building a graph from external data or
105    /// reconstructing from serialized state.
106    #[must_use]
107    pub fn from_components(
108        nodes: NodeArena,
109        edges: BidirectionalEdgeStore,
110        strings: StringInterner,
111        files: FileRegistry,
112        indices: AuxiliaryIndices,
113        macro_metadata: NodeMetadataStore,
114    ) -> Self {
115        Self {
116            nodes: Arc::new(nodes),
117            edges: Arc::new(edges),
118            strings: Arc::new(strings),
119            files: Arc::new(files),
120            indices: Arc::new(indices),
121            macro_metadata: Arc::new(macro_metadata),
122            node_provenance: Arc::new(NodeProvenanceStore::new()),
123            edge_provenance: Arc::new(EdgeProvenanceStore::new()),
124            fact_epoch: 0,
125            epoch: 0,
126            confidence: HashMap::new(),
127        }
128    }
129
130    /// Creates a cheap snapshot of the graph.
131    ///
132    /// This operation is O(5) Arc clones, which completes in <1μs.
133    /// The snapshot is isolated from future mutations to the original graph.
134    ///
135    /// # Example
136    ///
137    /// ```rust
138    /// use sqry_core::graph::unified::concurrent::CodeGraph;
139    ///
140    /// let graph = CodeGraph::new();
141    /// let snapshot = graph.snapshot();
142    /// // snapshot is independent of future mutations to graph
143    /// ```
144    #[must_use]
145    pub fn snapshot(&self) -> GraphSnapshot {
146        GraphSnapshot {
147            nodes: Arc::clone(&self.nodes),
148            edges: Arc::clone(&self.edges),
149            strings: Arc::clone(&self.strings),
150            files: Arc::clone(&self.files),
151            indices: Arc::clone(&self.indices),
152            macro_metadata: Arc::clone(&self.macro_metadata),
153            node_provenance: Arc::clone(&self.node_provenance),
154            edge_provenance: Arc::clone(&self.edge_provenance),
155            fact_epoch: self.fact_epoch,
156            epoch: self.epoch,
157        }
158    }
159
160    /// Returns a reference to the node arena.
161    #[inline]
162    #[must_use]
163    pub fn nodes(&self) -> &NodeArena {
164        &self.nodes
165    }
166
167    /// Returns a reference to the bidirectional edge store.
168    #[inline]
169    #[must_use]
170    pub fn edges(&self) -> &BidirectionalEdgeStore {
171        &self.edges
172    }
173
174    /// Returns a reference to the string interner.
175    #[inline]
176    #[must_use]
177    pub fn strings(&self) -> &StringInterner {
178        &self.strings
179    }
180
181    /// Returns a reference to the file registry.
182    #[inline]
183    #[must_use]
184    pub fn files(&self) -> &FileRegistry {
185        &self.files
186    }
187
188    /// Returns a reference to the auxiliary indices.
189    #[inline]
190    #[must_use]
191    pub fn indices(&self) -> &AuxiliaryIndices {
192        &self.indices
193    }
194
195    /// Returns a reference to the macro boundary metadata store.
196    #[inline]
197    #[must_use]
198    pub fn macro_metadata(&self) -> &NodeMetadataStore {
199        &self.macro_metadata
200    }
201
202    // ------------------------------------------------------------------
203    // Phase 1 fact-layer provenance accessors (P1U09).
204    // ------------------------------------------------------------------
205
206    /// Returns the monotonic fact-layer epoch stamped on the most recently
207    /// saved or loaded snapshot. Returns `0` for graphs that have not been
208    /// persisted yet or were loaded from V7 snapshots.
209    #[inline]
210    #[must_use]
211    pub fn fact_epoch(&self) -> u64 {
212        self.fact_epoch
213    }
214
215    /// Looks up node provenance by `NodeId`.
216    ///
217    /// Returns `None` if the `NodeId` is out of range, the slot is vacant,
218    /// or the stored generation does not match (stale handle).
219    #[inline]
220    #[must_use]
221    pub fn node_provenance(
222        &self,
223        id: crate::graph::unified::node::id::NodeId,
224    ) -> Option<&NodeProvenance> {
225        self.node_provenance.lookup(id)
226    }
227
228    /// Looks up edge provenance by `EdgeId`.
229    ///
230    /// Returns `None` if the `EdgeId` is out of range, the slot is vacant,
231    /// or the edge is the invalid sentinel.
232    #[inline]
233    #[must_use]
234    pub fn edge_provenance(
235        &self,
236        id: crate::graph::unified::edge::id::EdgeId,
237    ) -> Option<&EdgeProvenance> {
238        self.edge_provenance.lookup(id)
239    }
240
241    /// Returns a borrowed provenance view for a file.
242    ///
243    /// Returns `None` for invalid/unregistered `FileId`s.
244    #[inline]
245    #[must_use]
246    pub fn file_provenance(
247        &self,
248        id: crate::graph::unified::file::id::FileId,
249    ) -> Option<FileProvenanceView<'_>> {
250        self.files.file_provenance(id)
251    }
252
253    /// Sets the provenance stores and fact epoch, typically called by the
254    /// persistence loader after deserializing a V8 snapshot.
255    pub(crate) fn set_provenance(
256        &mut self,
257        node_provenance: NodeProvenanceStore,
258        edge_provenance: EdgeProvenanceStore,
259        fact_epoch: u64,
260    ) {
261        self.node_provenance = Arc::new(node_provenance);
262        self.edge_provenance = Arc::new(edge_provenance);
263        self.fact_epoch = fact_epoch;
264    }
265
266    /// Returns the current epoch.
267    #[inline]
268    #[must_use]
269    pub fn epoch(&self) -> u64 {
270        self.epoch
271    }
272
273    /// Returns a mutable reference to the node arena.
274    ///
275    /// Uses `Arc::make_mut` for copy-on-write semantics: if other
276    /// references exist (e.g., snapshots), the data is cloned.
277    #[inline]
278    pub fn nodes_mut(&mut self) -> &mut NodeArena {
279        Arc::make_mut(&mut self.nodes)
280    }
281
282    /// Returns a mutable reference to the bidirectional edge store.
283    ///
284    /// Uses `Arc::make_mut` for copy-on-write semantics.
285    #[inline]
286    pub fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore {
287        Arc::make_mut(&mut self.edges)
288    }
289
290    /// Returns a mutable reference to the string interner.
291    ///
292    /// Uses `Arc::make_mut` for copy-on-write semantics.
293    #[inline]
294    pub fn strings_mut(&mut self) -> &mut StringInterner {
295        Arc::make_mut(&mut self.strings)
296    }
297
298    /// Returns a mutable reference to the file registry.
299    ///
300    /// Uses `Arc::make_mut` for copy-on-write semantics.
301    #[inline]
302    pub fn files_mut(&mut self) -> &mut FileRegistry {
303        Arc::make_mut(&mut self.files)
304    }
305
306    /// Returns a mutable reference to the auxiliary indices.
307    ///
308    /// Uses `Arc::make_mut` for copy-on-write semantics.
309    #[inline]
310    pub fn indices_mut(&mut self) -> &mut AuxiliaryIndices {
311        Arc::make_mut(&mut self.indices)
312    }
313
314    /// Returns a mutable reference to the macro boundary metadata store.
315    ///
316    /// Uses `Arc::make_mut` for copy-on-write semantics.
317    #[inline]
318    pub fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore {
319        Arc::make_mut(&mut self.macro_metadata)
320    }
321
322    /// Returns mutable references to both the node arena and the string interner.
323    ///
324    /// This avoids the borrow-conflict that arises when calling `nodes_mut()` and
325    /// `strings_mut()` separately on `&mut self`.
326    #[inline]
327    pub fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner) {
328        (
329            Arc::make_mut(&mut self.nodes),
330            Arc::make_mut(&mut self.strings),
331        )
332    }
333
334    /// Rebuilds auxiliary indices from the current node arena.
335    ///
336    /// This avoids the borrow conflict that arises when calling `nodes()` and
337    /// `indices_mut()` separately on `&mut self`. Uses disjoint field borrowing
338    /// to access `nodes` (shared) and `indices` (mutable) simultaneously.
339    /// Internally calls `AuxiliaryIndices::build_from_arena` which clears
340    /// existing indices and rebuilds in a single pass without per-element
341    /// duplicate checking.
342    pub fn rebuild_indices(&mut self) {
343        let nodes = &self.nodes;
344        Arc::make_mut(&mut self.indices).build_from_arena(nodes);
345    }
346
347    /// Increments the epoch counter and returns the new value.
348    ///
349    /// Called automatically by `ConcurrentCodeGraph::write()`.
350    #[inline]
351    pub fn bump_epoch(&mut self) -> u64 {
352        self.epoch = self.epoch.wrapping_add(1);
353        self.epoch
354    }
355
356    /// Sets the epoch to a specific value.
357    ///
358    /// This is primarily for testing or reconstruction from serialized state.
359    #[inline]
360    pub fn set_epoch(&mut self, epoch: u64) {
361        self.epoch = epoch;
362    }
363
364    /// Returns the number of nodes in the graph.
365    ///
366    /// This is a convenience method that delegates to `nodes().len()`.
367    ///
368    /// # Example
369    ///
370    /// ```rust
371    /// use sqry_core::graph::unified::concurrent::CodeGraph;
372    ///
373    /// let graph = CodeGraph::new();
374    /// assert_eq!(graph.node_count(), 0);
375    /// ```
376    #[inline]
377    #[must_use]
378    pub fn node_count(&self) -> usize {
379        self.nodes.len()
380    }
381
382    /// Returns the number of edges in the graph (forward direction).
383    ///
384    /// This counts edges in the forward store, including both CSR and delta edges.
385    ///
386    /// # Example
387    ///
388    /// ```rust
389    /// use sqry_core::graph::unified::concurrent::CodeGraph;
390    ///
391    /// let graph = CodeGraph::new();
392    /// assert_eq!(graph.edge_count(), 0);
393    /// ```
394    #[inline]
395    #[must_use]
396    pub fn edge_count(&self) -> usize {
397        let stats = self.edges.stats();
398        stats.forward.csr_edge_count + stats.forward.delta_edge_count
399    }
400
401    /// Returns true if the graph contains no nodes.
402    ///
403    /// This is a convenience method that delegates to `nodes().is_empty()`.
404    ///
405    /// # Example
406    ///
407    /// ```rust
408    /// use sqry_core::graph::unified::concurrent::CodeGraph;
409    ///
410    /// let graph = CodeGraph::new();
411    /// assert!(graph.is_empty());
412    /// ```
413    #[inline]
414    #[must_use]
415    pub fn is_empty(&self) -> bool {
416        self.nodes.is_empty()
417    }
418
419    /// Returns an iterator over all indexed file paths.
420    ///
421    /// This is useful for enumerating all files that have been processed
422    /// and added to the graph.
423    ///
424    /// # Example
425    ///
426    /// ```rust
427    /// use sqry_core::graph::unified::concurrent::CodeGraph;
428    ///
429    /// let graph = CodeGraph::new();
430    /// for (file_id, path) in graph.indexed_files() {
431    ///     println!("File {}: {}", file_id.index(), path.display());
432    /// }
433    /// ```
434    #[inline]
435    pub fn indexed_files(
436        &self,
437    ) -> impl Iterator<Item = (crate::graph::unified::file::FileId, &std::path::Path)> {
438        self.files
439            .iter()
440            .map(|(id, arc_path)| (id, arc_path.as_ref()))
441    }
442
443    /// Returns the per-language confidence metadata.
444    ///
445    /// This contains analysis confidence information collected during graph build,
446    /// primarily used by language plugins (e.g., Rust) to track analysis quality.
447    #[inline]
448    #[must_use]
449    pub fn confidence(&self) -> &HashMap<String, ConfidenceMetadata> {
450        &self.confidence
451    }
452
453    /// Merges confidence metadata for a language.
454    ///
455    /// If confidence already exists for the language, this merges the new
456    /// metadata (taking the lower confidence level and combining limitations).
457    /// Otherwise, it inserts the new confidence.
458    pub fn merge_confidence(&mut self, language: &str, metadata: ConfidenceMetadata) {
459        use crate::confidence::ConfidenceLevel;
460
461        self.confidence
462            .entry(language.to_string())
463            .and_modify(|existing| {
464                // Take the lower confidence level (more conservative)
465                let new_level = match (&existing.level, &metadata.level) {
466                    (ConfidenceLevel::Verified, other) | (other, ConfidenceLevel::Verified) => {
467                        *other
468                    }
469                    (ConfidenceLevel::Partial, ConfidenceLevel::AstOnly)
470                    | (ConfidenceLevel::AstOnly, ConfidenceLevel::Partial) => {
471                        ConfidenceLevel::AstOnly
472                    }
473                    (level, _) => *level,
474                };
475                existing.level = new_level;
476
477                // Merge limitations (deduplicated)
478                for limitation in &metadata.limitations {
479                    if !existing.limitations.contains(limitation) {
480                        existing.limitations.push(limitation.clone());
481                    }
482                }
483
484                // Merge unavailable features (deduplicated)
485                for feature in &metadata.unavailable_features {
486                    if !existing.unavailable_features.contains(feature) {
487                        existing.unavailable_features.push(feature.clone());
488                    }
489                }
490            })
491            .or_insert(metadata);
492    }
493
494    /// Sets the confidence metadata map directly.
495    ///
496    /// This is primarily used when loading a graph from serialized state.
497    pub fn set_confidence(&mut self, confidence: HashMap<String, ConfidenceMetadata>) {
498        self.confidence = confidence;
499    }
500}
501
502impl Default for CodeGraph {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508impl fmt::Debug for CodeGraph {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        f.debug_struct("CodeGraph")
511            .field("nodes", &self.nodes.len())
512            .field("epoch", &self.epoch)
513            .finish_non_exhaustive()
514    }
515}
516
517impl GraphMemorySize for CodeGraph {
518    /// Estimates the total heap bytes owned by this `CodeGraph`.
519    ///
520    /// Sums heap usage across every component the graph owns: node arena,
521    /// bidirectional edge store (forward + reverse CSR + tombstones + delta
522    /// buffer), string interner, file registry, auxiliary indices, sparse
523    /// macro/classpath metadata, provenance stores, and the per-language
524    /// confidence map. Used by the `sqryd` daemon's admission controller
525    /// and workspace retention reaper to enforce memory budgets.
526    fn heap_bytes(&self) -> usize {
527        let mut confidence_bytes = self.confidence.capacity()
528            * (std::mem::size_of::<String>()
529                + std::mem::size_of::<ConfidenceMetadata>()
530                + HASHMAP_ENTRY_OVERHEAD);
531        for (key, meta) in &self.confidence {
532            confidence_bytes += key.capacity();
533            // `ConfidenceMetadata.limitations` / `unavailable_features` are
534            // `Vec<String>`; charge their spill and inner String payloads.
535            confidence_bytes += meta.limitations.capacity() * std::mem::size_of::<String>();
536            for s in &meta.limitations {
537                confidence_bytes += s.capacity();
538            }
539            confidence_bytes +=
540                meta.unavailable_features.capacity() * std::mem::size_of::<String>();
541            for s in &meta.unavailable_features {
542                confidence_bytes += s.capacity();
543            }
544        }
545
546        self.nodes.heap_bytes()
547            + self.edges.heap_bytes()
548            + self.strings.heap_bytes()
549            + self.files.heap_bytes()
550            + self.indices.heap_bytes()
551            + self.macro_metadata.heap_bytes()
552            + self.node_provenance.heap_bytes()
553            + self.edge_provenance.heap_bytes()
554            + confidence_bytes
555    }
556}
557
558/// Thread-safe wrapper for `CodeGraph` with epoch versioning.
559///
560/// `ConcurrentCodeGraph` provides MVCC-style concurrency:
561/// - Multiple readers can access the graph simultaneously
562/// - Only one writer can hold the lock at a time
563/// - Each write operation increments the epoch for cursor invalidation
564///
565/// # Design
566///
567/// The wrapper uses `parking_lot::RwLock` for efficient locking:
568/// - Fair scheduling prevents writer starvation
569/// - No poisoning (unlike `std::sync::RwLock`)
570/// - Faster lock/unlock operations
571///
572/// # Usage
573///
574/// ```rust
575/// use sqry_core::graph::unified::concurrent::ConcurrentCodeGraph;
576///
577/// let graph = ConcurrentCodeGraph::new();
578///
579/// // Read access (multiple readers allowed)
580/// {
581///     let guard = graph.read();
582///     let _nodes = guard.nodes();
583/// }
584///
585/// // Write access (exclusive)
586/// {
587///     let mut guard = graph.write();
588///     let _nodes = guard.nodes_mut();
589/// }
590///
591/// // Snapshot for long queries
592/// let snapshot = graph.snapshot();
593/// ```
594pub struct ConcurrentCodeGraph {
595    /// The underlying code graph protected by a read-write lock.
596    inner: RwLock<CodeGraph>,
597    /// Global epoch counter for cursor validation.
598    epoch: AtomicU64,
599}
600
601impl ConcurrentCodeGraph {
602    /// Creates a new empty `ConcurrentCodeGraph`.
603    #[must_use]
604    pub fn new() -> Self {
605        Self {
606            inner: RwLock::new(CodeGraph::new()),
607            epoch: AtomicU64::new(0),
608        }
609    }
610
611    /// Creates a `ConcurrentCodeGraph` from an existing `CodeGraph`.
612    #[must_use]
613    pub fn from_graph(graph: CodeGraph) -> Self {
614        let epoch = graph.epoch();
615        Self {
616            inner: RwLock::new(graph),
617            epoch: AtomicU64::new(epoch),
618        }
619    }
620
621    /// Acquires a read lock on the graph.
622    ///
623    /// Multiple readers can hold the lock simultaneously.
624    /// This does not increment the epoch.
625    #[inline]
626    pub fn read(&self) -> RwLockReadGuard<'_, CodeGraph> {
627        self.inner.read()
628    }
629
630    /// Acquires a write lock on the graph.
631    ///
632    /// Only one writer can hold the lock at a time.
633    /// This increments the global epoch counter.
634    #[inline]
635    pub fn write(&self) -> RwLockWriteGuard<'_, CodeGraph> {
636        // Increment the global epoch
637        self.epoch.fetch_add(1, Ordering::SeqCst);
638        let mut guard = self.inner.write();
639        // Sync the inner graph's epoch with the global epoch
640        guard.set_epoch(self.epoch.load(Ordering::SeqCst));
641        guard
642    }
643
644    /// Returns the current global epoch.
645    ///
646    /// This can be used to detect if the graph has been modified
647    /// since a previous operation (cursor invalidation).
648    #[inline]
649    #[must_use]
650    pub fn epoch(&self) -> u64 {
651        self.epoch.load(Ordering::SeqCst)
652    }
653
654    /// Creates a cheap snapshot of the graph.
655    ///
656    /// This acquires a brief read lock to clone the Arc references.
657    /// The snapshot is isolated from future mutations.
658    #[must_use]
659    pub fn snapshot(&self) -> GraphSnapshot {
660        self.inner.read().snapshot()
661    }
662
663    // ------------------------------------------------------------------
664    // Phase 1 fact-layer provenance convenience accessors.
665    // These acquire a brief read lock, matching the snapshot() pattern.
666    // ------------------------------------------------------------------
667
668    /// Returns the monotonic fact-layer epoch from the underlying graph.
669    #[must_use]
670    pub fn fact_epoch(&self) -> u64 {
671        self.inner.read().fact_epoch()
672    }
673
674    /// Looks up node provenance by `NodeId` (acquires a brief read lock).
675    #[must_use]
676    pub fn node_provenance(
677        &self,
678        id: crate::graph::unified::node::id::NodeId,
679    ) -> Option<NodeProvenance> {
680        self.inner.read().node_provenance(id).copied()
681    }
682
683    /// Looks up edge provenance by `EdgeId` (acquires a brief read lock).
684    #[must_use]
685    pub fn edge_provenance(
686        &self,
687        id: crate::graph::unified::edge::id::EdgeId,
688    ) -> Option<EdgeProvenance> {
689        self.inner.read().edge_provenance(id).copied()
690    }
691
692    /// Returns a file provenance view (acquires a brief read lock).
693    ///
694    /// Returns an owned copy since the borrow cannot outlive the lock guard.
695    #[must_use]
696    pub fn file_provenance(
697        &self,
698        id: crate::graph::unified::file::id::FileId,
699    ) -> Option<OwnedFileProvenanceView> {
700        let guard = self.inner.read();
701        guard.file_provenance(id).map(|v| OwnedFileProvenanceView {
702            content_hash: *v.content_hash,
703            indexed_at: v.indexed_at,
704            source_uri: v.source_uri,
705            is_external: v.is_external,
706        })
707    }
708
709    /// Attempts to acquire a read lock without blocking.
710    ///
711    /// Returns `None` if the lock is currently held exclusively.
712    #[inline]
713    #[must_use]
714    pub fn try_read(&self) -> Option<RwLockReadGuard<'_, CodeGraph>> {
715        self.inner.try_read()
716    }
717
718    /// Attempts to acquire a write lock without blocking.
719    ///
720    /// Returns `None` if the lock is currently held by another thread.
721    /// If successful, increments the epoch.
722    #[inline]
723    pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, CodeGraph>> {
724        self.inner.try_write().map(|mut guard| {
725            self.epoch.fetch_add(1, Ordering::SeqCst);
726            guard.set_epoch(self.epoch.load(Ordering::SeqCst));
727            guard
728        })
729    }
730}
731
732impl Default for ConcurrentCodeGraph {
733    fn default() -> Self {
734        Self::new()
735    }
736}
737
738impl fmt::Debug for ConcurrentCodeGraph {
739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740        f.debug_struct("ConcurrentCodeGraph")
741            .field("epoch", &self.epoch.load(Ordering::SeqCst))
742            .finish_non_exhaustive()
743    }
744}
745
746/// Owned copy of file provenance, returned by [`ConcurrentCodeGraph::file_provenance`]
747/// because the borrowed [`FileProvenanceView`] cannot outlive the read-lock guard.
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
749pub struct OwnedFileProvenanceView {
750    /// SHA-256 of the on-disk file bytes (owned copy).
751    pub content_hash: [u8; 32],
752    /// Unix-epoch seconds at which this file was registered.
753    pub indexed_at: u64,
754    /// Optional interned physical origin URI.
755    pub source_uri: Option<StringId>,
756    /// Whether this file originates from an external source.
757    pub is_external: bool,
758}
759
760/// Immutable snapshot of a `CodeGraph` for long-running queries.
761///
762/// `GraphSnapshot` holds Arc references to the graph components,
763/// providing a consistent view that is isolated from concurrent mutations.
764///
765/// # Design
766///
767/// Snapshots are created via `CodeGraph::snapshot()` or
768/// `ConcurrentCodeGraph::snapshot()`. They are:
769///
770/// - **Immutable**: No mutation methods available
771/// - **Isolated**: Independent of future graph mutations
772/// - **Cheap**: Only Arc clones, no data copying
773/// - **Self-contained**: Can outlive the original graph/lock
774///
775/// # Usage
776///
777/// ```rust
778/// use sqry_core::graph::unified::concurrent::{ConcurrentCodeGraph, GraphSnapshot};
779///
780/// let graph = ConcurrentCodeGraph::new();
781///
782/// // Create snapshot for a long query
783/// let snapshot: GraphSnapshot = graph.snapshot();
784///
785/// // Snapshot can be used independently
786/// let _epoch = snapshot.epoch();
787/// ```
788#[derive(Clone)]
789pub struct GraphSnapshot {
790    /// Node storage snapshot.
791    nodes: Arc<NodeArena>,
792    /// Edge storage snapshot.
793    edges: Arc<BidirectionalEdgeStore>,
794    /// String interner snapshot.
795    strings: Arc<StringInterner>,
796    /// File registry snapshot.
797    files: Arc<FileRegistry>,
798    /// Auxiliary indices snapshot.
799    indices: Arc<AuxiliaryIndices>,
800    /// Sparse macro boundary metadata snapshot.
801    macro_metadata: Arc<NodeMetadataStore>,
802    /// Dense node provenance snapshot (Phase 1).
803    node_provenance: Arc<NodeProvenanceStore>,
804    /// Dense edge provenance snapshot (Phase 1).
805    edge_provenance: Arc<EdgeProvenanceStore>,
806    /// Monotonic fact-layer epoch at snapshot time.
807    fact_epoch: u64,
808    /// Epoch at snapshot time (for cursor validation).
809    epoch: u64,
810}
811
812impl GraphSnapshot {
813    /// Returns a reference to the node arena.
814    #[inline]
815    #[must_use]
816    pub fn nodes(&self) -> &NodeArena {
817        &self.nodes
818    }
819
820    /// Returns a reference to the bidirectional edge store.
821    #[inline]
822    #[must_use]
823    pub fn edges(&self) -> &BidirectionalEdgeStore {
824        &self.edges
825    }
826
827    /// Returns a reference to the string interner.
828    #[inline]
829    #[must_use]
830    pub fn strings(&self) -> &StringInterner {
831        &self.strings
832    }
833
834    /// Returns a reference to the file registry.
835    #[inline]
836    #[must_use]
837    pub fn files(&self) -> &FileRegistry {
838        &self.files
839    }
840
841    /// Returns a reference to the auxiliary indices.
842    #[inline]
843    #[must_use]
844    pub fn indices(&self) -> &AuxiliaryIndices {
845        &self.indices
846    }
847
848    /// Returns a reference to the macro boundary metadata store.
849    #[inline]
850    #[must_use]
851    pub fn macro_metadata(&self) -> &NodeMetadataStore {
852        &self.macro_metadata
853    }
854
855    // ------------------------------------------------------------------
856    // Phase 1 fact-layer provenance accessors (P1U09).
857    // ------------------------------------------------------------------
858
859    /// Returns the monotonic fact-layer epoch.
860    #[inline]
861    #[must_use]
862    pub fn fact_epoch(&self) -> u64 {
863        self.fact_epoch
864    }
865
866    /// Looks up node provenance by `NodeId`.
867    #[inline]
868    #[must_use]
869    pub fn node_provenance(
870        &self,
871        id: crate::graph::unified::node::id::NodeId,
872    ) -> Option<&NodeProvenance> {
873        self.node_provenance.lookup(id)
874    }
875
876    /// Looks up edge provenance by `EdgeId`.
877    #[inline]
878    #[must_use]
879    pub fn edge_provenance(
880        &self,
881        id: crate::graph::unified::edge::id::EdgeId,
882    ) -> Option<&EdgeProvenance> {
883        self.edge_provenance.lookup(id)
884    }
885
886    /// Returns a borrowed provenance view for a file.
887    #[inline]
888    #[must_use]
889    pub fn file_provenance(
890        &self,
891        id: crate::graph::unified::file::id::FileId,
892    ) -> Option<FileProvenanceView<'_>> {
893        self.files.file_provenance(id)
894    }
895
896    /// Returns the epoch at which this snapshot was taken.
897    ///
898    /// This can be compared against the current graph epoch to
899    /// detect if the graph has changed since the snapshot.
900    #[inline]
901    #[must_use]
902    pub fn epoch(&self) -> u64 {
903        self.epoch
904    }
905
906    /// Returns `true` if this snapshot's epoch matches the given epoch.
907    ///
908    /// Use this to validate cursors before continuing pagination.
909    #[inline]
910    #[must_use]
911    pub fn epoch_matches(&self, other_epoch: u64) -> bool {
912        self.epoch == other_epoch
913    }
914
915    // ============================================================================
916    // Query Methods
917    // ============================================================================
918
919    /// Finds nodes matching a pattern.
920    ///
921    /// Performs a simple substring match on node names and qualified names.
922    /// Returns all matching node IDs.
923    ///
924    /// # Performance
925    ///
926    /// Optimized to iterate over unique strings in the interner (smaller set)
927    /// rather than all nodes in the arena.
928    ///
929    /// # Arguments
930    ///
931    /// * `pattern` - The pattern to match (substring search)
932    ///
933    /// # Returns
934    ///
935    /// A vector of `NodeIds` for all matching nodes.
936    #[must_use]
937    pub fn find_by_pattern(&self, pattern: &str) -> Vec<crate::graph::unified::node::NodeId> {
938        let mut matches = Vec::new();
939
940        // 1. Scan unique strings in interner for matches
941        for (str_id, s) in self.strings.iter() {
942            if s.contains(pattern) {
943                // 2. If string matches, look up all nodes with this name
944                // Check qualified name index
945                matches.extend_from_slice(self.indices.by_qualified_name(str_id));
946                // Check simple name index
947                matches.extend_from_slice(self.indices.by_name(str_id));
948            }
949        }
950
951        // Deduplicate matches (a node might match both qualified and simple name)
952        matches.sort_unstable();
953        matches.dedup();
954
955        matches
956    }
957
958    /// Gets all callees of a node (functions called by this node).
959    ///
960    /// Queries the forward edge store for all Calls edges from this node.
961    ///
962    /// # Arguments
963    ///
964    /// * `node` - The node ID to query
965    ///
966    /// # Returns
967    ///
968    /// A vector of `NodeIds` representing functions called by this node.
969    #[must_use]
970    pub fn get_callees(
971        &self,
972        node: crate::graph::unified::node::NodeId,
973    ) -> Vec<crate::graph::unified::node::NodeId> {
974        use crate::graph::unified::edge::EdgeKind;
975
976        self.edges
977            .edges_from(node)
978            .into_iter()
979            .filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
980            .map(|edge| edge.target)
981            .collect()
982    }
983
984    /// Gets all callers of a node (functions that call this node).
985    ///
986    /// Queries the reverse edge store for all Calls edges to this node.
987    ///
988    /// # Arguments
989    ///
990    /// * `node` - The node ID to query
991    ///
992    /// # Returns
993    ///
994    /// A vector of `NodeIds` representing functions that call this node.
995    #[must_use]
996    pub fn get_callers(
997        &self,
998        node: crate::graph::unified::node::NodeId,
999    ) -> Vec<crate::graph::unified::node::NodeId> {
1000        use crate::graph::unified::edge::EdgeKind;
1001
1002        self.edges
1003            .edges_to(node)
1004            .into_iter()
1005            .filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
1006            .map(|edge| edge.source)
1007            .collect()
1008    }
1009
1010    /// Iterates over all nodes in the graph.
1011    ///
1012    /// Returns an iterator yielding (`NodeId`, &`NodeEntry`) pairs for all
1013    /// occupied slots in the arena.
1014    ///
1015    /// # Returns
1016    ///
1017    /// An iterator over (`NodeId`, &`NodeEntry`) pairs.
1018    pub fn iter_nodes(
1019        &self,
1020    ) -> impl Iterator<
1021        Item = (
1022            crate::graph::unified::node::NodeId,
1023            &crate::graph::unified::storage::arena::NodeEntry,
1024        ),
1025    > {
1026        self.nodes.iter()
1027    }
1028
1029    /// Iterates over all edges in the graph.
1030    ///
1031    /// Returns an iterator yielding (source, target, `EdgeKind`) tuples for
1032    /// all edges in the forward edge store.
1033    ///
1034    /// # Returns
1035    ///
1036    /// An iterator over edge tuples.
1037    pub fn iter_edges(
1038        &self,
1039    ) -> impl Iterator<
1040        Item = (
1041            crate::graph::unified::node::NodeId,
1042            crate::graph::unified::node::NodeId,
1043            crate::graph::unified::edge::EdgeKind,
1044        ),
1045    > + '_ {
1046        // Iterate over all nodes in the arena and get their outgoing edges
1047        self.nodes.iter().flat_map(move |(node_id, _entry)| {
1048            // Get all edges from this node
1049            self.edges
1050                .edges_from(node_id)
1051                .into_iter()
1052                .map(move |edge| (node_id, edge.target, edge.kind))
1053        })
1054    }
1055
1056    /// Gets a node entry by ID.
1057    ///
1058    /// Returns a reference to the `NodeEntry` if the ID is valid, or None
1059    /// if the ID is invalid or stale.
1060    ///
1061    /// # Arguments
1062    ///
1063    /// * `id` - The node ID to look up
1064    ///
1065    /// # Returns
1066    ///
1067    /// A reference to the `NodeEntry`, or None if not found.
1068    #[must_use]
1069    pub fn get_node(
1070        &self,
1071        id: crate::graph::unified::node::NodeId,
1072    ) -> Option<&crate::graph::unified::storage::arena::NodeEntry> {
1073        self.nodes.get(id)
1074    }
1075}
1076
1077impl fmt::Debug for GraphSnapshot {
1078    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1079        f.debug_struct("GraphSnapshot")
1080            .field("nodes", &self.nodes.len())
1081            .field("epoch", &self.epoch)
1082            .finish_non_exhaustive()
1083    }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089    use crate::graph::unified::{
1090        FileScope, NodeId, ResolutionMode, SymbolCandidateOutcome, SymbolQuery,
1091        SymbolResolutionOutcome,
1092    };
1093
1094    fn resolve_symbol_strict(snapshot: &GraphSnapshot, symbol: &str) -> Option<NodeId> {
1095        match snapshot.resolve_symbol(&SymbolQuery {
1096            symbol,
1097            file_scope: FileScope::Any,
1098            mode: ResolutionMode::Strict,
1099        }) {
1100            SymbolResolutionOutcome::Resolved(node_id) => Some(node_id),
1101            SymbolResolutionOutcome::NotFound
1102            | SymbolResolutionOutcome::FileNotIndexed
1103            | SymbolResolutionOutcome::Ambiguous(_) => None,
1104        }
1105    }
1106
1107    fn candidate_nodes(snapshot: &GraphSnapshot, symbol: &str) -> Vec<NodeId> {
1108        match snapshot.find_symbol_candidates(&SymbolQuery {
1109            symbol,
1110            file_scope: FileScope::Any,
1111            mode: ResolutionMode::AllowSuffixCandidates,
1112        }) {
1113            SymbolCandidateOutcome::Candidates(candidates) => candidates,
1114            SymbolCandidateOutcome::NotFound | SymbolCandidateOutcome::FileNotIndexed => Vec::new(),
1115        }
1116    }
1117
1118    #[test]
1119    fn test_code_graph_new() {
1120        let graph = CodeGraph::new();
1121        assert_eq!(graph.epoch(), 0);
1122        assert_eq!(graph.nodes().len(), 0);
1123    }
1124
1125    #[test]
1126    fn test_code_graph_default() {
1127        let graph = CodeGraph::default();
1128        assert_eq!(graph.epoch(), 0);
1129    }
1130
1131    #[test]
1132    fn test_code_graph_snapshot() {
1133        let graph = CodeGraph::new();
1134        let snapshot = graph.snapshot();
1135        assert_eq!(snapshot.epoch(), 0);
1136        assert_eq!(snapshot.nodes().len(), 0);
1137    }
1138
1139    #[test]
1140    fn test_code_graph_bump_epoch() {
1141        let mut graph = CodeGraph::new();
1142        assert_eq!(graph.epoch(), 0);
1143        assert_eq!(graph.bump_epoch(), 1);
1144        assert_eq!(graph.epoch(), 1);
1145        assert_eq!(graph.bump_epoch(), 2);
1146        assert_eq!(graph.epoch(), 2);
1147    }
1148
1149    #[test]
1150    fn test_code_graph_set_epoch() {
1151        let mut graph = CodeGraph::new();
1152        graph.set_epoch(42);
1153        assert_eq!(graph.epoch(), 42);
1154    }
1155
1156    #[test]
1157    fn test_code_graph_from_components() {
1158        let nodes = NodeArena::new();
1159        let edges = BidirectionalEdgeStore::new();
1160        let strings = StringInterner::new();
1161        let files = FileRegistry::new();
1162        let indices = AuxiliaryIndices::new();
1163        let macro_metadata = NodeMetadataStore::new();
1164
1165        let graph =
1166            CodeGraph::from_components(nodes, edges, strings, files, indices, macro_metadata);
1167        assert_eq!(graph.epoch(), 0);
1168    }
1169
1170    #[test]
1171    fn test_code_graph_mut_accessors() {
1172        let mut graph = CodeGraph::new();
1173
1174        // Access mutable references - should not panic
1175        let _nodes = graph.nodes_mut();
1176        let _edges = graph.edges_mut();
1177        let _strings = graph.strings_mut();
1178        let _files = graph.files_mut();
1179        let _indices = graph.indices_mut();
1180    }
1181
1182    #[test]
1183    fn test_code_graph_snapshot_isolation() {
1184        let mut graph = CodeGraph::new();
1185        let snapshot1 = graph.snapshot();
1186
1187        // Mutate the graph
1188        graph.bump_epoch();
1189
1190        let snapshot2 = graph.snapshot();
1191
1192        // Snapshots should have different epochs
1193        assert_eq!(snapshot1.epoch(), 0);
1194        assert_eq!(snapshot2.epoch(), 1);
1195    }
1196
1197    #[test]
1198    fn test_code_graph_debug() {
1199        let graph = CodeGraph::new();
1200        let debug_str = format!("{graph:?}");
1201        assert!(debug_str.contains("CodeGraph"));
1202        assert!(debug_str.contains("epoch"));
1203    }
1204
1205    /// Exact-byte regression for the iter2 fix of CodeGraph.confidence
1206    /// accounting: adding a ConfidenceMetadata entry with known inner
1207    /// Vec<String> capacities must increase heap_bytes() by exactly the sum
1208    /// of those capacities plus Vec<String> slot overhead.
1209    #[test]
1210    fn test_codegraph_heap_bytes_counts_confidence_inner_strings() {
1211        let mut graph = CodeGraph::new();
1212        // Seed one confidence entry so the HashMap has non-zero capacity;
1213        // reserve slack so the next insert cannot rehash.
1214        graph.set_confidence({
1215            let mut m = HashMap::with_capacity(8);
1216            m.insert("seed".to_string(), ConfidenceMetadata::default());
1217            m
1218        });
1219        let before = graph.heap_bytes();
1220        let before_cap = graph.confidence.capacity();
1221
1222        let lim1 = String::from("no type inference");
1223        let lim2 = String::from("no generic specialization");
1224        let feat1 = String::from("rust-analyzer");
1225        let l1 = lim1.capacity();
1226        let l2 = lim2.capacity();
1227        let f1 = feat1.capacity();
1228
1229        let limitations = vec![lim1, lim2];
1230        let lim_vec_cap = limitations.capacity();
1231
1232        let unavailable_features = vec![feat1];
1233        let feat_vec_cap = unavailable_features.capacity();
1234
1235        let key = String::from("rust");
1236        let key_cap = key.capacity();
1237        graph.confidence.insert(
1238            key,
1239            ConfidenceMetadata {
1240                limitations,
1241                unavailable_features,
1242                ..Default::default()
1243            },
1244        );
1245        assert_eq!(
1246            graph.confidence.capacity(),
1247            before_cap,
1248            "prerequisite: confidence HashMap must not rehash during the test insert",
1249        );
1250
1251        let after = graph.heap_bytes();
1252        let expected_inner = key_cap
1253            + lim_vec_cap * std::mem::size_of::<String>()
1254            + l1
1255            + l2
1256            + feat_vec_cap * std::mem::size_of::<String>()
1257            + f1;
1258        assert_eq!(
1259            after - before,
1260            expected_inner,
1261            "CodeGraph::heap_bytes must count ConfidenceMetadata inner Vec<String> capacity exactly",
1262        );
1263    }
1264
1265    #[test]
1266    fn test_codegraph_heap_bytes_grows_with_content() {
1267        use crate::graph::unified::node::NodeKind;
1268        use crate::graph::unified::storage::arena::NodeEntry;
1269        use std::path::Path;
1270
1271        // An empty graph reports some heap bytes (FileRegistry seeds index 0
1272        // with `vec![None]`, HashMaps have non-zero base capacity once touched,
1273        // etc.) but the value must be finite and well under the 100 MB cap.
1274        let empty = CodeGraph::new();
1275        let empty_bytes = empty.heap_bytes();
1276        assert!(
1277            empty_bytes < 100 * 1024 * 1024,
1278            "empty graph heap_bytes should be <100 MiB, got {empty_bytes}"
1279        );
1280
1281        let mut graph = CodeGraph::new();
1282        for i in 0..32u32 {
1283            let name = format!("sym_{i}");
1284            let qual = format!("module::sym_{i}");
1285            let file = format!("file_{i}.rs");
1286
1287            let name_id = graph.strings_mut().intern(&name).unwrap();
1288            let qual_id = graph.strings_mut().intern(&qual).unwrap();
1289            let file_id = graph.files_mut().register(Path::new(&file)).unwrap();
1290
1291            let entry =
1292                NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(qual_id);
1293            let node_id = graph.nodes_mut().alloc(entry).unwrap();
1294            graph
1295                .indices_mut()
1296                .add(node_id, NodeKind::Function, name_id, Some(qual_id), file_id);
1297        }
1298
1299        let populated_bytes = graph.heap_bytes();
1300        assert!(
1301            populated_bytes > 0,
1302            "populated graph should report nonzero heap bytes"
1303        );
1304        assert!(
1305            populated_bytes > empty_bytes,
1306            "populated graph ({populated_bytes}) should exceed empty graph ({empty_bytes})"
1307        );
1308        assert!(
1309            populated_bytes < 100 * 1024 * 1024,
1310            "test graph heap_bytes should be <100 MiB, got {populated_bytes}"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_concurrent_code_graph_new() {
1316        let graph = ConcurrentCodeGraph::new();
1317        assert_eq!(graph.epoch(), 0);
1318    }
1319
1320    #[test]
1321    fn test_concurrent_code_graph_default() {
1322        let graph = ConcurrentCodeGraph::default();
1323        assert_eq!(graph.epoch(), 0);
1324    }
1325
1326    #[test]
1327    fn test_concurrent_code_graph_from_graph() {
1328        let mut inner = CodeGraph::new();
1329        inner.set_epoch(10);
1330        let graph = ConcurrentCodeGraph::from_graph(inner);
1331        assert_eq!(graph.epoch(), 10);
1332    }
1333
1334    #[test]
1335    fn test_concurrent_code_graph_read() {
1336        let graph = ConcurrentCodeGraph::new();
1337        let guard = graph.read();
1338        assert_eq!(guard.epoch(), 0);
1339        assert_eq!(guard.nodes().len(), 0);
1340    }
1341
1342    #[test]
1343    fn test_concurrent_code_graph_write_increments_epoch() {
1344        let graph = ConcurrentCodeGraph::new();
1345        assert_eq!(graph.epoch(), 0);
1346
1347        {
1348            let guard = graph.write();
1349            assert_eq!(guard.epoch(), 1);
1350        }
1351
1352        assert_eq!(graph.epoch(), 1);
1353
1354        {
1355            let _guard = graph.write();
1356        }
1357
1358        assert_eq!(graph.epoch(), 2);
1359    }
1360
1361    #[test]
1362    fn test_concurrent_code_graph_snapshot() {
1363        let graph = ConcurrentCodeGraph::new();
1364
1365        {
1366            let _guard = graph.write();
1367        }
1368
1369        let snapshot = graph.snapshot();
1370        assert_eq!(snapshot.epoch(), 1);
1371    }
1372
1373    #[test]
1374    fn test_concurrent_code_graph_try_read() {
1375        let graph = ConcurrentCodeGraph::new();
1376        let guard = graph.try_read();
1377        assert!(guard.is_some());
1378    }
1379
1380    #[test]
1381    fn test_concurrent_code_graph_try_write() {
1382        let graph = ConcurrentCodeGraph::new();
1383        let guard = graph.try_write();
1384        assert!(guard.is_some());
1385        assert_eq!(graph.epoch(), 1);
1386    }
1387
1388    #[test]
1389    fn test_concurrent_code_graph_debug() {
1390        let graph = ConcurrentCodeGraph::new();
1391        let debug_str = format!("{graph:?}");
1392        assert!(debug_str.contains("ConcurrentCodeGraph"));
1393        assert!(debug_str.contains("epoch"));
1394    }
1395
1396    #[test]
1397    fn test_graph_snapshot_accessors() {
1398        let graph = CodeGraph::new();
1399        let snapshot = graph.snapshot();
1400
1401        // All accessors should work
1402        let _nodes = snapshot.nodes();
1403        let _edges = snapshot.edges();
1404        let _strings = snapshot.strings();
1405        let _files = snapshot.files();
1406        let _indices = snapshot.indices();
1407        let _epoch = snapshot.epoch();
1408    }
1409
1410    #[test]
1411    fn test_graph_snapshot_epoch_matches() {
1412        let graph = CodeGraph::new();
1413        let snapshot = graph.snapshot();
1414
1415        assert!(snapshot.epoch_matches(0));
1416        assert!(!snapshot.epoch_matches(1));
1417    }
1418
1419    #[test]
1420    fn test_graph_snapshot_clone() {
1421        let graph = CodeGraph::new();
1422        let snapshot1 = graph.snapshot();
1423        let snapshot2 = snapshot1.clone();
1424
1425        assert_eq!(snapshot1.epoch(), snapshot2.epoch());
1426    }
1427
1428    #[test]
1429    fn test_graph_snapshot_debug() {
1430        let graph = CodeGraph::new();
1431        let snapshot = graph.snapshot();
1432        let debug_str = format!("{snapshot:?}");
1433        assert!(debug_str.contains("GraphSnapshot"));
1434        assert!(debug_str.contains("epoch"));
1435    }
1436
1437    #[test]
1438    fn test_multiple_readers() {
1439        let graph = ConcurrentCodeGraph::new();
1440
1441        // Multiple readers should be able to acquire locks simultaneously
1442        let guard1 = graph.read();
1443        let guard2 = graph.read();
1444        let guard3 = graph.read();
1445
1446        assert_eq!(guard1.epoch(), 0);
1447        assert_eq!(guard2.epoch(), 0);
1448        assert_eq!(guard3.epoch(), 0);
1449    }
1450
1451    #[test]
1452    fn test_code_graph_clone() {
1453        let mut graph = CodeGraph::new();
1454        graph.bump_epoch();
1455
1456        let cloned = graph.clone();
1457        assert_eq!(cloned.epoch(), 1);
1458    }
1459
1460    #[test]
1461    fn test_epoch_wrapping() {
1462        let mut graph = CodeGraph::new();
1463        graph.set_epoch(u64::MAX);
1464        let new_epoch = graph.bump_epoch();
1465        assert_eq!(new_epoch, 0); // Should wrap around
1466    }
1467
1468    // ============================================================================
1469    // Query method tests
1470    // ============================================================================
1471
1472    #[test]
1473    fn test_snapshot_resolve_symbol() {
1474        use crate::graph::unified::node::NodeKind;
1475        use crate::graph::unified::storage::arena::NodeEntry;
1476        use std::path::Path;
1477
1478        let mut graph = CodeGraph::new();
1479
1480        // Add some nodes with qualified names
1481        let name_id = graph.strings_mut().intern("test_func").unwrap();
1482        let qual_name_id = graph.strings_mut().intern("module::test_func").unwrap();
1483        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1484
1485        let entry =
1486            NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(qual_name_id);
1487
1488        let node_id = graph.nodes_mut().alloc(entry).unwrap();
1489        graph.indices_mut().add(
1490            node_id,
1491            NodeKind::Function,
1492            name_id,
1493            Some(qual_name_id),
1494            file_id,
1495        );
1496
1497        let snapshot = graph.snapshot();
1498
1499        // Find by qualified name
1500        let found = resolve_symbol_strict(&snapshot, "module::test_func");
1501        assert_eq!(found, Some(node_id));
1502
1503        // Find by exact simple name
1504        let found2 = resolve_symbol_strict(&snapshot, "test_func");
1505        assert_eq!(found2, Some(node_id));
1506
1507        // Not found
1508        assert!(resolve_symbol_strict(&snapshot, "nonexistent").is_none());
1509    }
1510
1511    #[test]
1512    fn test_snapshot_find_by_pattern() {
1513        use crate::graph::unified::node::NodeKind;
1514        use crate::graph::unified::storage::arena::NodeEntry;
1515        use std::path::Path;
1516
1517        let mut graph = CodeGraph::new();
1518
1519        // Add nodes with different names
1520        let name1 = graph.strings_mut().intern("foo_bar").unwrap();
1521        let name2 = graph.strings_mut().intern("baz_bar").unwrap();
1522        let name3 = graph.strings_mut().intern("qux_test").unwrap();
1523        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1524
1525        let node1 = graph
1526            .nodes_mut()
1527            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1528            .unwrap();
1529        let node2 = graph
1530            .nodes_mut()
1531            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1532            .unwrap();
1533        let node3 = graph
1534            .nodes_mut()
1535            .alloc(NodeEntry::new(NodeKind::Function, name3, file_id))
1536            .unwrap();
1537
1538        graph
1539            .indices_mut()
1540            .add(node1, NodeKind::Function, name1, None, file_id);
1541        graph
1542            .indices_mut()
1543            .add(node2, NodeKind::Function, name2, None, file_id);
1544        graph
1545            .indices_mut()
1546            .add(node3, NodeKind::Function, name3, None, file_id);
1547
1548        let snapshot = graph.snapshot();
1549
1550        // Find by pattern
1551        let matches = snapshot.find_by_pattern("bar");
1552        assert_eq!(matches.len(), 2);
1553        assert!(matches.contains(&node1));
1554        assert!(matches.contains(&node2));
1555
1556        // Find single match
1557        let matches = snapshot.find_by_pattern("qux");
1558        assert_eq!(matches.len(), 1);
1559        assert_eq!(matches[0], node3);
1560
1561        // No matches
1562        let matches = snapshot.find_by_pattern("nonexistent");
1563        assert!(matches.is_empty());
1564    }
1565
1566    #[test]
1567    fn test_snapshot_get_callees() {
1568        use crate::graph::unified::edge::EdgeKind;
1569        use crate::graph::unified::node::NodeKind;
1570        use crate::graph::unified::storage::arena::NodeEntry;
1571        use std::path::Path;
1572
1573        let mut graph = CodeGraph::new();
1574
1575        // Create caller and callee nodes
1576        let caller_name = graph.strings_mut().intern("caller").unwrap();
1577        let callee1_name = graph.strings_mut().intern("callee1").unwrap();
1578        let callee2_name = graph.strings_mut().intern("callee2").unwrap();
1579        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1580
1581        let caller_id = graph
1582            .nodes_mut()
1583            .alloc(NodeEntry::new(NodeKind::Function, caller_name, file_id))
1584            .unwrap();
1585        let callee1_id = graph
1586            .nodes_mut()
1587            .alloc(NodeEntry::new(NodeKind::Function, callee1_name, file_id))
1588            .unwrap();
1589        let callee2_id = graph
1590            .nodes_mut()
1591            .alloc(NodeEntry::new(NodeKind::Function, callee2_name, file_id))
1592            .unwrap();
1593
1594        // Add call edges
1595        graph.edges_mut().add_edge(
1596            caller_id,
1597            callee1_id,
1598            EdgeKind::Calls {
1599                argument_count: 0,
1600                is_async: false,
1601            },
1602            file_id,
1603        );
1604        graph.edges_mut().add_edge(
1605            caller_id,
1606            callee2_id,
1607            EdgeKind::Calls {
1608                argument_count: 0,
1609                is_async: false,
1610            },
1611            file_id,
1612        );
1613
1614        let snapshot = graph.snapshot();
1615
1616        // Query callees
1617        let callees = snapshot.get_callees(caller_id);
1618        assert_eq!(callees.len(), 2);
1619        assert!(callees.contains(&callee1_id));
1620        assert!(callees.contains(&callee2_id));
1621
1622        // Node with no callees
1623        let callees = snapshot.get_callees(callee1_id);
1624        assert!(callees.is_empty());
1625    }
1626
1627    #[test]
1628    fn test_snapshot_get_callers() {
1629        use crate::graph::unified::edge::EdgeKind;
1630        use crate::graph::unified::node::NodeKind;
1631        use crate::graph::unified::storage::arena::NodeEntry;
1632        use std::path::Path;
1633
1634        let mut graph = CodeGraph::new();
1635
1636        // Create caller and callee nodes
1637        let caller1_name = graph.strings_mut().intern("caller1").unwrap();
1638        let caller2_name = graph.strings_mut().intern("caller2").unwrap();
1639        let callee_name = graph.strings_mut().intern("callee").unwrap();
1640        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1641
1642        let caller1_id = graph
1643            .nodes_mut()
1644            .alloc(NodeEntry::new(NodeKind::Function, caller1_name, file_id))
1645            .unwrap();
1646        let caller2_id = graph
1647            .nodes_mut()
1648            .alloc(NodeEntry::new(NodeKind::Function, caller2_name, file_id))
1649            .unwrap();
1650        let callee_id = graph
1651            .nodes_mut()
1652            .alloc(NodeEntry::new(NodeKind::Function, callee_name, file_id))
1653            .unwrap();
1654
1655        // Add call edges
1656        graph.edges_mut().add_edge(
1657            caller1_id,
1658            callee_id,
1659            EdgeKind::Calls {
1660                argument_count: 0,
1661                is_async: false,
1662            },
1663            file_id,
1664        );
1665        graph.edges_mut().add_edge(
1666            caller2_id,
1667            callee_id,
1668            EdgeKind::Calls {
1669                argument_count: 0,
1670                is_async: false,
1671            },
1672            file_id,
1673        );
1674
1675        let snapshot = graph.snapshot();
1676
1677        // Query callers
1678        let callers = snapshot.get_callers(callee_id);
1679        assert_eq!(callers.len(), 2);
1680        assert!(callers.contains(&caller1_id));
1681        assert!(callers.contains(&caller2_id));
1682
1683        // Node with no callers
1684        let callers = snapshot.get_callers(caller1_id);
1685        assert!(callers.is_empty());
1686    }
1687
1688    #[test]
1689    fn test_snapshot_find_symbol_candidates() {
1690        use crate::graph::unified::node::NodeKind;
1691        use crate::graph::unified::storage::arena::NodeEntry;
1692        use std::path::Path;
1693
1694        let mut graph = CodeGraph::new();
1695
1696        // Add nodes with same symbol name but different qualified names
1697        let symbol_name = graph.strings_mut().intern("test").unwrap();
1698        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1699
1700        let node1 = graph
1701            .nodes_mut()
1702            .alloc(NodeEntry::new(NodeKind::Function, symbol_name, file_id))
1703            .unwrap();
1704        let node2 = graph
1705            .nodes_mut()
1706            .alloc(NodeEntry::new(NodeKind::Method, symbol_name, file_id))
1707            .unwrap();
1708
1709        // Add a different symbol
1710        let other_name = graph.strings_mut().intern("other").unwrap();
1711        let node3 = graph
1712            .nodes_mut()
1713            .alloc(NodeEntry::new(NodeKind::Function, other_name, file_id))
1714            .unwrap();
1715
1716        graph
1717            .indices_mut()
1718            .add(node1, NodeKind::Function, symbol_name, None, file_id);
1719        graph
1720            .indices_mut()
1721            .add(node2, NodeKind::Method, symbol_name, None, file_id);
1722        graph
1723            .indices_mut()
1724            .add(node3, NodeKind::Function, other_name, None, file_id);
1725
1726        let snapshot = graph.snapshot();
1727
1728        // Find by symbol
1729        let matches = candidate_nodes(&snapshot, "test");
1730        assert_eq!(matches.len(), 2);
1731        assert!(matches.contains(&node1));
1732        assert!(matches.contains(&node2));
1733
1734        // Find other symbol
1735        let matches = candidate_nodes(&snapshot, "other");
1736        assert_eq!(matches.len(), 1);
1737        assert_eq!(matches[0], node3);
1738
1739        // No matches
1740        let matches = candidate_nodes(&snapshot, "nonexistent");
1741        assert!(matches.is_empty());
1742    }
1743
1744    #[test]
1745    fn test_snapshot_iter_nodes() {
1746        use crate::graph::unified::node::NodeKind;
1747        use crate::graph::unified::storage::arena::NodeEntry;
1748        use std::path::Path;
1749
1750        let mut graph = CodeGraph::new();
1751
1752        // Add some nodes
1753        let name1 = graph.strings_mut().intern("func1").unwrap();
1754        let name2 = graph.strings_mut().intern("func2").unwrap();
1755        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1756
1757        let node1 = graph
1758            .nodes_mut()
1759            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1760            .unwrap();
1761        let node2 = graph
1762            .nodes_mut()
1763            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1764            .unwrap();
1765
1766        let snapshot = graph.snapshot();
1767
1768        // Iterate nodes
1769        let snapshot_nodes: Vec<_> = snapshot.iter_nodes().collect();
1770        assert_eq!(snapshot_nodes.len(), 2);
1771
1772        let node_ids: Vec<_> = snapshot_nodes.iter().map(|(id, _)| *id).collect();
1773        assert!(node_ids.contains(&node1));
1774        assert!(node_ids.contains(&node2));
1775    }
1776
1777    #[test]
1778    fn test_snapshot_iter_edges() {
1779        use crate::graph::unified::edge::EdgeKind;
1780        use crate::graph::unified::node::NodeKind;
1781        use crate::graph::unified::storage::arena::NodeEntry;
1782        use std::path::Path;
1783
1784        let mut graph = CodeGraph::new();
1785
1786        // Create nodes
1787        let name1 = graph.strings_mut().intern("func1").unwrap();
1788        let name2 = graph.strings_mut().intern("func2").unwrap();
1789        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1790
1791        let node1 = graph
1792            .nodes_mut()
1793            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1794            .unwrap();
1795        let node2 = graph
1796            .nodes_mut()
1797            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1798            .unwrap();
1799
1800        // Add edges
1801        graph.edges_mut().add_edge(
1802            node1,
1803            node2,
1804            EdgeKind::Calls {
1805                argument_count: 0,
1806                is_async: false,
1807            },
1808            file_id,
1809        );
1810
1811        let snapshot = graph.snapshot();
1812
1813        // Iterate edges
1814        let edges: Vec<_> = snapshot.iter_edges().collect();
1815        assert_eq!(edges.len(), 1);
1816
1817        let (src, tgt, kind) = &edges[0];
1818        assert_eq!(*src, node1);
1819        assert_eq!(*tgt, node2);
1820        assert!(matches!(
1821            kind,
1822            EdgeKind::Calls {
1823                argument_count: 0,
1824                is_async: false
1825            }
1826        ));
1827    }
1828
1829    #[test]
1830    fn test_snapshot_get_node() {
1831        use crate::graph::unified::node::NodeId;
1832        use crate::graph::unified::node::NodeKind;
1833        use crate::graph::unified::storage::arena::NodeEntry;
1834        use std::path::Path;
1835
1836        let mut graph = CodeGraph::new();
1837
1838        // Add a node
1839        let name = graph.strings_mut().intern("test_func").unwrap();
1840        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1841
1842        let node_id = graph
1843            .nodes_mut()
1844            .alloc(NodeEntry::new(NodeKind::Function, name, file_id))
1845            .unwrap();
1846
1847        let snapshot = graph.snapshot();
1848
1849        // Get node
1850        let entry = snapshot.get_node(node_id);
1851        assert!(entry.is_some());
1852        assert_eq!(entry.unwrap().kind, NodeKind::Function);
1853
1854        // Invalid node
1855        let invalid_id = NodeId::INVALID;
1856        assert!(snapshot.get_node(invalid_id).is_none());
1857    }
1858
1859    #[test]
1860    fn test_snapshot_query_empty_graph() {
1861        use crate::graph::unified::node::NodeId;
1862
1863        let graph = CodeGraph::new();
1864        let snapshot = graph.snapshot();
1865
1866        // All queries should return empty on empty graph
1867        assert!(resolve_symbol_strict(&snapshot, "test").is_none());
1868        assert!(snapshot.find_by_pattern("test").is_empty());
1869        assert!(candidate_nodes(&snapshot, "test").is_empty());
1870
1871        let dummy_id = NodeId::new(0, 1);
1872        assert!(snapshot.get_callees(dummy_id).is_empty());
1873        assert!(snapshot.get_callers(dummy_id).is_empty());
1874
1875        assert_eq!(snapshot.iter_nodes().count(), 0);
1876        assert_eq!(snapshot.iter_edges().count(), 0);
1877    }
1878
1879    #[test]
1880    fn test_snapshot_edge_filtering_by_kind() {
1881        use crate::graph::unified::edge::EdgeKind;
1882        use crate::graph::unified::node::NodeKind;
1883        use crate::graph::unified::storage::arena::NodeEntry;
1884        use std::path::Path;
1885
1886        let mut graph = CodeGraph::new();
1887
1888        // Create nodes
1889        let name1 = graph.strings_mut().intern("func1").unwrap();
1890        let name2 = graph.strings_mut().intern("func2").unwrap();
1891        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1892
1893        let node1 = graph
1894            .nodes_mut()
1895            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1896            .unwrap();
1897        let node2 = graph
1898            .nodes_mut()
1899            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1900            .unwrap();
1901
1902        // Add different kinds of edges
1903        graph.edges_mut().add_edge(
1904            node1,
1905            node2,
1906            EdgeKind::Calls {
1907                argument_count: 0,
1908                is_async: false,
1909            },
1910            file_id,
1911        );
1912        graph
1913            .edges_mut()
1914            .add_edge(node1, node2, EdgeKind::References, file_id);
1915
1916        let snapshot = graph.snapshot();
1917
1918        // get_callees should only return Calls edges
1919        let callees = snapshot.get_callees(node1);
1920        assert_eq!(callees.len(), 1);
1921        assert_eq!(callees[0], node2);
1922
1923        // iter_edges returns all edges regardless of kind
1924        let edges: Vec<_> = snapshot.iter_edges().collect();
1925        assert_eq!(edges.len(), 2);
1926    }
1927}