Skip to main content

llm_agent_runtime/
graph.rs

1//! # Module: Graph
2//!
3//! ## Responsibility
4//! Provides an in-memory knowledge graph with typed entities and relationships.
5//! Mirrors the public API of `mem-graph`.
6//!
7//! ## Guarantees
8//! - Thread-safe: `GraphStore` wraps state in `Arc<Mutex<_>>`
9//! - BFS/DFS traversal and shortest-path are correct for directed graphs
10//! - Non-panicking: all operations return `Result`
11//!
12//! ## NOT Responsible For
13//! - Persistence to disk or external store
14//! - Graph sharding / distributed graphs
15
16use crate::error::AgentRuntimeError;
17use crate::util::recover_lock;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
21use std::sync::{Arc, Mutex};
22
23// ── OrdF32 newtype ─────────────────────────────────────────────────────────────
24
25/// Newtype wrapper for `f32` that implements `Ord`.
26/// NaN is treated as `Greater` for safe use in a `BinaryHeap`.
27#[derive(Debug, Clone, Copy, PartialEq)]
28struct OrdF32(f32);
29
30impl Eq for OrdF32 {}
31
32impl PartialOrd for OrdF32 {
33    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
34        Some(self.cmp(other))
35    }
36}
37
38impl Ord for OrdF32 {
39    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40        self.0
41            .partial_cmp(&other.0)
42            .unwrap_or(std::cmp::Ordering::Greater)
43    }
44}
45
46// ── EntityId ──────────────────────────────────────────────────────────────────
47
48/// Stable identifier for a graph entity.
49#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50pub struct EntityId(pub String);
51
52impl EntityId {
53    /// Create a new `EntityId` from any string-like value.
54    ///
55    /// # Panics (debug only)
56    ///
57    /// Triggers a `debug_assert!` if `id` is empty.  In release builds a
58    /// `tracing::warn!` is emitted instead so that the misconfiguration is
59    /// surfaced in production logs without aborting the process.
60    pub fn new(id: impl Into<String>) -> Self {
61        let id = id.into();
62        if id.is_empty() {
63            debug_assert!(false, "EntityId must not be empty");
64            tracing::warn!("EntityId::new called with an empty string — entity IDs should be non-empty to avoid lookup ambiguity");
65        }
66        Self(id)
67    }
68
69    /// Create a validated `EntityId`, returning an error if `id` is empty.
70    pub fn try_new(id: impl Into<String>) -> Result<Self, AgentRuntimeError> {
71        let id = id.into();
72        if id.is_empty() {
73            return Err(AgentRuntimeError::Graph(
74                "EntityId must not be empty".into(),
75            ));
76        }
77        Ok(Self(id))
78    }
79
80    /// Return the inner ID string as a `&str`.
81    pub fn as_str(&self) -> &str {
82        &self.0
83    }
84
85    /// Return `true` if the inner ID string is empty.
86    ///
87    /// Note: `EntityId::new` warns (debug: asserts) against empty IDs.
88    pub fn is_empty(&self) -> bool {
89        self.0.is_empty()
90    }
91
92    /// Return `true` if the inner ID string starts with `prefix`.
93    pub fn starts_with(&self, prefix: &str) -> bool {
94        self.0.starts_with(prefix)
95    }
96}
97
98impl AsRef<str> for EntityId {
99    fn as_ref(&self) -> &str {
100        &self.0
101    }
102}
103
104impl std::fmt::Display for EntityId {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{}", self.0)
107    }
108}
109
110// ── Entity ────────────────────────────────────────────────────────────────────
111
112/// A node in the knowledge graph.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Entity {
115    /// Unique identifier.
116    pub id: EntityId,
117    /// Human-readable label (e.g. "Person", "Concept").
118    pub label: String,
119    /// Arbitrary key-value properties.
120    pub properties: HashMap<String, Value>,
121}
122
123impl Entity {
124    /// Construct a new entity with no properties.
125    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
126        Self {
127            id: EntityId::new(id),
128            label: label.into(),
129            properties: HashMap::new(),
130        }
131    }
132
133    /// Construct a new entity with the given properties.
134    pub fn with_properties(
135        id: impl Into<String>,
136        label: impl Into<String>,
137        properties: HashMap<String, Value>,
138    ) -> Self {
139        Self {
140            id: EntityId::new(id),
141            label: label.into(),
142            properties,
143        }
144    }
145
146    /// Add a single key-value property, consuming and returning `self`.
147    ///
148    /// Allows fluent builder-style construction:
149    /// ```rust,ignore
150    /// let e = Entity::new("alice", "Person")
151    ///     .with_property("age", 30.into())
152    ///     .with_property("role", "engineer".into());
153    /// ```
154    pub fn with_property(mut self, key: impl Into<String>, value: Value) -> Self {
155        self.properties.insert(key.into(), value);
156        self
157    }
158
159    /// Return `true` if the entity has a property with the given key.
160    pub fn has_property(&self, key: &str) -> bool {
161        self.properties.contains_key(key)
162    }
163
164    /// Return a reference to the property value for `key`, or `None` if absent.
165    pub fn property_value(&self, key: &str) -> Option<&serde_json::Value> {
166        self.properties.get(key)
167    }
168}
169
170// ── Relationship ──────────────────────────────────────────────────────────────
171
172/// A directed, typed edge between two entities.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct Relationship {
175    /// Source entity.
176    pub from: EntityId,
177    /// Target entity.
178    pub to: EntityId,
179    /// Relationship type label (e.g. "KNOWS", "PART_OF").
180    pub kind: String,
181    /// Optional weight for weighted-graph use cases.
182    pub weight: f32,
183}
184
185impl Relationship {
186    /// Construct a new relationship with the given kind and weight.
187    pub fn new(
188        from: impl Into<String>,
189        to: impl Into<String>,
190        kind: impl Into<String>,
191        weight: f32,
192    ) -> Self {
193        Self {
194            from: EntityId::new(from),
195            to: EntityId::new(to),
196            kind: kind.into(),
197            weight,
198        }
199    }
200
201    /// Return `true` if this relationship is a self-loop (`from == to`).
202    pub fn is_self_loop(&self) -> bool {
203        self.from == self.to
204    }
205
206    /// Return a new `Relationship` with `from` and `to` swapped.
207    ///
208    /// The `kind` and `weight` are preserved unchanged.
209    pub fn reversed(&self) -> Self {
210        Self {
211            from: self.to.clone(),
212            to: self.from.clone(),
213            kind: self.kind.clone(),
214            weight: self.weight,
215        }
216    }
217}
218
219// ── MemGraphError (mirrors upstream) ─────────────────────────────────────────
220
221/// Graph-specific errors, mirrors `mem-graph::MemGraphError`.
222#[derive(Debug, thiserror::Error)]
223pub enum MemGraphError {
224    /// The requested entity was not found.
225    #[error("Entity '{0}' not found")]
226    EntityNotFound(String),
227
228    /// A relationship between the two entities already exists with the same kind.
229    #[error("Relationship '{kind}' from '{from}' to '{to}' already exists")]
230    DuplicateRelationship {
231        /// Source entity ID.
232        from: String,
233        /// Target entity ID.
234        to: String,
235        /// Relationship kind label.
236        kind: String,
237    },
238
239    /// Generic internal error.
240    #[error("Graph internal error: {0}")]
241    Internal(String),
242}
243
244impl From<MemGraphError> for AgentRuntimeError {
245    fn from(e: MemGraphError) -> Self {
246        AgentRuntimeError::Graph(e.to_string())
247    }
248}
249
250// ── GraphStore ────────────────────────────────────────────────────────────────
251
252/// In-memory knowledge graph supporting entities, relationships, BFS/DFS,
253/// shortest-path, weighted shortest-path, and graph analytics.
254///
255/// ## Guarantees
256/// - Thread-safe via `Arc<Mutex<_>>`
257/// - BFS/DFS are non-recursive (stack-safe)
258/// - Shortest-path is hop-count based (BFS)
259/// - Weighted shortest-path uses Dijkstra's algorithm
260#[derive(Debug, Clone)]
261pub struct GraphStore {
262    inner: Arc<Mutex<GraphInner>>,
263}
264
265#[derive(Debug)]
266struct GraphInner {
267    entities: HashMap<EntityId, Entity>,
268    /// Flat list kept for full-edge iteration (degree/betweenness/label-propagation).
269    relationships: Vec<Relationship>,
270    /// Adjacency index: entity → outgoing relationships.
271    /// Kept in sync with `relationships` to give O(degree) neighbour lookup
272    /// in BFS/DFS/shortest-path without a full linear scan.
273    adjacency: HashMap<EntityId, Vec<Relationship>>,
274    /// Reverse adjacency index: entity → set of entities with edges pointing TO it.
275    /// Enables O(in-degree) `in_degree`, `source_nodes`, and `isolates` without a
276    /// full O(|E|) scan.
277    reverse_adjacency: HashMap<EntityId, Vec<EntityId>>,
278    /// Cached result of cycle detection. Invalidated on any mutation.
279    cycle_cache: Option<bool>,
280}
281
282impl GraphStore {
283    /// Create a new, empty graph store.
284    pub fn new() -> Self {
285        Self {
286            inner: Arc::new(Mutex::new(GraphInner {
287                entities: HashMap::new(),
288                relationships: Vec::new(),
289                adjacency: HashMap::new(),
290                reverse_adjacency: HashMap::new(),
291                cycle_cache: None,
292            })),
293        }
294    }
295
296    /// Add an entity to the graph.
297    ///
298    /// If an entity with the same ID already exists, it is replaced.
299    pub fn add_entity(&self, entity: Entity) -> Result<(), AgentRuntimeError> {
300        let mut inner = recover_lock(self.inner.lock(), "add_entity");
301        inner.cycle_cache = None;
302        // Ensure an adjacency entry exists even if the entity has no outgoing edges.
303        inner.adjacency.entry(entity.id.clone()).or_default();
304        inner.entities.insert(entity.id.clone(), entity);
305        Ok(())
306    }
307
308    /// Retrieve an entity by ID.
309    pub fn get_entity(&self, id: &EntityId) -> Result<Entity, AgentRuntimeError> {
310        let inner = recover_lock(self.inner.lock(), "get_entity");
311        inner
312            .entities
313            .get(id)
314            .cloned()
315            .ok_or_else(|| AgentRuntimeError::Graph(format!("entity '{}' not found", id.0)))
316    }
317
318    /// Return `true` if an entity with the given `id` exists in the graph.
319    pub fn has_entity(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
320        let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity");
321        Ok(inner.entities.contains_key(id))
322    }
323
324    /// Return `true` if the graph contains at least one entity.
325    pub fn has_any_entities(&self) -> Result<bool, AgentRuntimeError> {
326        let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_entities");
327        Ok(!inner.entities.is_empty())
328    }
329
330    /// Return the number of distinct entity label strings present in the graph.
331    pub fn entity_type_count(&self) -> Result<usize, AgentRuntimeError> {
332        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_type_count");
333        let types: std::collections::HashSet<&str> =
334            inner.entities.values().map(|e| e.label.as_str()).collect();
335        Ok(types.len())
336    }
337
338    /// Return the distinct entity labels present in the graph, sorted.
339    pub fn labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
340        let inner = recover_lock(self.inner.lock(), "GraphStore::labels");
341        let mut labels: Vec<String> = inner
342            .entities
343            .values()
344            .map(|e| e.label.clone())
345            .collect::<std::collections::HashSet<_>>()
346            .into_iter()
347            .collect();
348        labels.sort();
349        Ok(labels)
350    }
351
352    /// Return the number of incoming relationships for the given entity.
353    ///
354    /// Returns `0` if the entity has no in-edges or does not exist.
355    pub fn incoming_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
356        let inner = recover_lock(self.inner.lock(), "GraphStore::incoming_count_for");
357        Ok(inner.reverse_adjacency.get(id).map_or(0, |v| v.len()))
358    }
359
360    /// Return the number of outbound edges from the given entity.
361    ///
362    /// Returns `0` if the entity has no outgoing relationships.
363    pub fn outgoing_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
364        let inner = recover_lock(self.inner.lock(), "GraphStore::outgoing_count_for");
365        Ok(inner.adjacency.get(id).map_or(0, |v| v.len()))
366    }
367
368    /// Return the count of entities that have no outgoing edges (sink nodes).
369    pub fn sink_count(&self) -> Result<usize, AgentRuntimeError> {
370        let inner = recover_lock(self.inner.lock(), "GraphStore::sink_count");
371        let count = inner
372            .entities
373            .keys()
374            .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
375            .count();
376        Ok(count)
377    }
378
379    /// Return the count of entity pairs `(A, B)` where edges exist in both directions.
380    ///
381    /// Each bidirectional pair is counted once.
382    pub fn bidirectional_count(&self) -> Result<usize, AgentRuntimeError> {
383        let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_count");
384        let mut count = 0usize;
385        for (from, rels) in &inner.adjacency {
386            for rel in rels {
387                let to = &rel.to;
388                if to > from {
389                    // Only count when to > from to avoid double-counting
390                    if inner.adjacency.get(to).map_or(false, |v| v.iter().any(|r| &r.to == from)) {
391                        count += 1;
392                    }
393                }
394            }
395        }
396        Ok(count)
397    }
398
399    /// Return `true` if any relationship is a self-loop (`from == to`).
400    pub fn has_self_loops(&self) -> Result<bool, AgentRuntimeError> {
401        let inner = recover_lock(self.inner.lock(), "GraphStore::has_self_loops");
402        let found = inner.adjacency.iter().any(|(from, rels)| {
403            rels.iter().any(|r| &r.to == from)
404        });
405        Ok(found)
406    }
407
408    /// Return the count of entities that have no incoming edges (source nodes).
409    pub fn source_count(&self) -> Result<usize, AgentRuntimeError> {
410        let inner = recover_lock(self.inner.lock(), "GraphStore::source_count");
411        let count = inner
412            .entities
413            .keys()
414            .filter(|id| inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty()))
415            .count();
416        Ok(count)
417    }
418
419    /// Return the number of entities that have no outgoing relationships.
420    pub fn orphan_count(&self) -> Result<usize, AgentRuntimeError> {
421        let inner = recover_lock(self.inner.lock(), "GraphStore::orphan_count");
422        let count = inner
423            .entities
424            .keys()
425            .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
426            .count();
427        Ok(count)
428    }
429
430    /// Return the count of entities that have neither outgoing nor incoming relationships.
431    pub fn isolated_entity_count(&self) -> Result<usize, AgentRuntimeError> {
432        let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_entity_count");
433        let count = inner.entities.keys().filter(|id| {
434            inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
435                && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
436        }).count();
437        Ok(count)
438    }
439
440    /// Return the mean weight of all relationships in the graph.
441    ///
442    /// Returns `0.0` if no relationships have been added.
443    pub fn avg_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
444        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_relationship_weight");
445        if inner.relationships.is_empty() {
446            return Ok(0.0);
447        }
448        let total: f32 = inner.relationships.iter().map(|r| r.weight).sum();
449        Ok(total as f64 / inner.relationships.len() as f64)
450    }
451
452    /// Return the sum of in-degrees across all entities.
453    ///
454    /// Equal to the total number of relationships in the graph (each relationship
455    /// contributes 1 to the in-degree of its target entity).
456    pub fn total_in_degree(&self) -> Result<usize, AgentRuntimeError> {
457        let inner = recover_lock(self.inner.lock(), "GraphStore::total_in_degree");
458        Ok(inner.relationships.len())
459    }
460
461    /// Return the count of directed relationships from `from` to `to`.
462    ///
463    /// Returns `0` if no such relationships exist.
464    pub fn relationship_count_between(
465        &self,
466        from: &EntityId,
467        to: &EntityId,
468    ) -> Result<usize, AgentRuntimeError> {
469        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_between");
470        Ok(inner
471            .adjacency
472            .get(from)
473            .map_or(0, |rels| rels.iter().filter(|r| &r.to == to).count()))
474    }
475
476    /// Return all relationships originating from `id`, in insertion order.
477    ///
478    /// Returns an empty `Vec` if the entity has no outgoing relationships or does not exist.
479    pub fn edges_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
480        let inner = recover_lock(self.inner.lock(), "GraphStore::edges_from");
481        Ok(inner
482            .adjacency
483            .get(id)
484            .cloned()
485            .unwrap_or_default())
486    }
487
488    /// Return the `EntityId`s reachable from `id` in a single hop, sorted.
489    ///
490    /// Returns an empty `Vec` if the entity has no outgoing relationships or does not exist.
491    pub fn neighbors_of(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
492        let inner = recover_lock(self.inner.lock(), "GraphStore::neighbors_of");
493        let mut targets: Vec<EntityId> = inner
494            .adjacency
495            .get(id)
496            .map_or_else(Vec::new, |rels| rels.iter().map(|r| r.to.clone()).collect());
497        targets.sort_unstable();
498        targets.dedup();
499        Ok(targets)
500    }
501
502    /// Add a directed relationship between two existing entities.
503    ///
504    /// Both source and target entities must already exist in the graph.
505    pub fn add_relationship(&self, rel: Relationship) -> Result<(), AgentRuntimeError> {
506        let mut inner = recover_lock(self.inner.lock(), "add_relationship");
507
508        if !inner.entities.contains_key(&rel.from) {
509            return Err(AgentRuntimeError::Graph(format!(
510                "source entity '{}' not found",
511                rel.from.0
512            )));
513        }
514        if !inner.entities.contains_key(&rel.to) {
515            return Err(AgentRuntimeError::Graph(format!(
516                "target entity '{}' not found",
517                rel.to.0
518            )));
519        }
520
521        // Reject duplicate (from, to, kind) triples — the DuplicateRelationship
522        // error variant existed but was never raised, silently allowing duplicate
523        // edges that corrupt relationship_count() and BFS/DFS result counts.
524        let duplicate = inner
525            .relationships
526            .iter()
527            .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
528        if duplicate {
529            return Err(AgentRuntimeError::Graph(
530                MemGraphError::DuplicateRelationship {
531                    from: rel.from.0.clone(),
532                    to: rel.to.0.clone(),
533                    kind: rel.kind.clone(),
534                }
535                .to_string(),
536            ));
537        }
538
539        inner.cycle_cache = None;
540        // Keep adjacency in sync: add to outgoing list for rel.from.
541        inner
542            .adjacency
543            .entry(rel.from.clone())
544            .or_default()
545            .push(rel.clone());
546        // Keep reverse adjacency in sync: add rel.from to incoming list for rel.to.
547        inner
548            .reverse_adjacency
549            .entry(rel.to.clone())
550            .or_default()
551            .push(rel.from.clone());
552        inner.relationships.push(rel);
553        Ok(())
554    }
555
556    /// Remove a specific relationship by (from, to, kind).
557    ///
558    /// Returns `Err` if no matching relationship exists.
559    pub fn remove_relationship(
560        &self,
561        from: &EntityId,
562        to: &EntityId,
563        kind: &str,
564    ) -> Result<(), AgentRuntimeError> {
565        let mut inner = recover_lock(self.inner.lock(), "remove_relationship");
566
567        let before = inner.relationships.len();
568        inner
569            .relationships
570            .retain(|r| !(&r.from == from && &r.to == to && r.kind == kind));
571        if inner.relationships.len() == before {
572            return Err(AgentRuntimeError::Graph(format!(
573                "relationship '{kind}' from '{}' to '{}' not found",
574                from.0, to.0
575            )));
576        }
577
578        // Keep adjacency in sync.
579        if let Some(adj) = inner.adjacency.get_mut(from) {
580            adj.retain(|r| !(&r.to == to && r.kind == kind));
581        }
582        // Keep reverse adjacency in sync.
583        if let Some(rev) = inner.reverse_adjacency.get_mut(to) {
584            rev.retain(|src| src != from);
585        }
586
587        inner.cycle_cache = None;
588        Ok(())
589    }
590
591    /// Remove an entity and all relationships involving it.
592    pub fn remove_entity(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
593        let mut inner = recover_lock(self.inner.lock(), "remove_entity");
594
595        if inner.entities.remove(id).is_none() {
596            return Err(AgentRuntimeError::Graph(format!(
597                "entity '{}' not found",
598                id.0
599            )));
600        }
601        inner.cycle_cache = None;
602        inner.relationships.retain(|r| &r.from != id && &r.to != id);
603        // Remove outgoing edges for this entity and scrub it from others' lists.
604        inner.adjacency.remove(id);
605        for adj in inner.adjacency.values_mut() {
606            adj.retain(|r| &r.to != id);
607        }
608        // Remove incoming edges for this entity from the reverse adjacency index.
609        inner.reverse_adjacency.remove(id);
610        for rev in inner.reverse_adjacency.values_mut() {
611            rev.retain(|src| src != id);
612        }
613        Ok(())
614    }
615
616    /// Return all direct outgoing neighbours of the given entity (BFS layer 1).
617    ///
618    /// Uses the adjacency index for O(degree) lookup instead of O(|edges|).
619    fn neighbours(adjacency: &HashMap<EntityId, Vec<Relationship>>, id: &EntityId) -> Vec<EntityId> {
620        adjacency
621            .get(id)
622            .map(|rels| rels.iter().map(|r| r.to.clone()).collect())
623            .unwrap_or_default()
624    }
625
626    /// Breadth-first search starting from `start`.
627    ///
628    /// Returns entity IDs in BFS discovery order (not including the start node).
629    #[tracing::instrument(skip(self))]
630    pub fn bfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
631        let inner = recover_lock(self.inner.lock(), "bfs");
632
633        if !inner.entities.contains_key(start) {
634            return Err(AgentRuntimeError::Graph(format!(
635                "start entity '{}' not found",
636                start.0
637            )));
638        }
639
640        let mut visited: HashSet<EntityId> = HashSet::new();
641        let mut queue: VecDeque<EntityId> = VecDeque::new();
642        let mut result: Vec<EntityId> = Vec::new();
643
644        visited.insert(start.clone());
645        queue.push_back(start.clone());
646
647        while let Some(current) = queue.pop_front() {
648            let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, &current);
649            for neighbour in neighbours {
650                if visited.insert(neighbour.clone()) {
651                    result.push(neighbour.clone());
652                    queue.push_back(neighbour);
653                }
654            }
655        }
656
657        tracing::debug!("BFS visited {} nodes", result.len());
658        Ok(result)
659    }
660
661    /// Depth-first search starting from `start`.
662    ///
663    /// Returns entity IDs in DFS discovery order (not including the start node).
664    #[tracing::instrument(skip(self))]
665    pub fn dfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
666        let inner = recover_lock(self.inner.lock(), "dfs");
667
668        if !inner.entities.contains_key(start) {
669            return Err(AgentRuntimeError::Graph(format!(
670                "start entity '{}' not found",
671                start.0
672            )));
673        }
674
675        let mut visited: HashSet<EntityId> = HashSet::new();
676        let mut stack: Vec<EntityId> = Vec::new();
677        let mut result: Vec<EntityId> = Vec::new();
678
679        visited.insert(start.clone());
680        stack.push(start.clone());
681
682        while let Some(current) = stack.pop() {
683            let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, &current);
684            for neighbour in neighbours {
685                if visited.insert(neighbour.clone()) {
686                    result.push(neighbour.clone());
687                    stack.push(neighbour);
688                }
689            }
690        }
691
692        tracing::debug!("DFS visited {} nodes", result.len());
693        Ok(result)
694    }
695
696    /// Find the shortest path (by hop count) between `from` and `to`.
697    ///
698    /// # Returns
699    /// - `Some(path)` — ordered list of `EntityId`s from `from` to `to` (inclusive)
700    /// - `None` — no path exists
701    #[tracing::instrument(skip(self))]
702    pub fn shortest_path(
703        &self,
704        from: &EntityId,
705        to: &EntityId,
706    ) -> Result<Option<Vec<EntityId>>, AgentRuntimeError> {
707        let inner = recover_lock(self.inner.lock(), "shortest_path");
708
709        if !inner.entities.contains_key(from) {
710            return Err(AgentRuntimeError::Graph(format!(
711                "source entity '{}' not found",
712                from.0
713            )));
714        }
715        if !inner.entities.contains_key(to) {
716            return Err(AgentRuntimeError::Graph(format!(
717                "target entity '{}' not found",
718                to.0
719            )));
720        }
721
722        if from == to {
723            return Ok(Some(vec![from.clone()]));
724        }
725
726        // Item 5 — predecessor-map BFS; O(1) per enqueue instead of O(path_len).
727        let mut visited: HashSet<EntityId> = HashSet::new();
728        let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
729        let mut queue: VecDeque<EntityId> = VecDeque::new();
730
731        visited.insert(from.clone());
732        queue.push_back(from.clone());
733
734        while let Some(current) = queue.pop_front() {
735            for neighbour in Self::neighbours(&inner.adjacency, &current) {
736                if &neighbour == to {
737                    // Reconstruct path by following prev back from current.
738                    let mut path = vec![neighbour, current.clone()];
739                    let mut node = current;
740                    while let Some(p) = prev.get(&node) {
741                        path.push(p.clone());
742                        node = p.clone();
743                    }
744                    path.reverse();
745                    return Ok(Some(path));
746                }
747                if visited.insert(neighbour.clone()) {
748                    prev.insert(neighbour.clone(), current.clone());
749                    queue.push_back(neighbour);
750                }
751            }
752        }
753
754        Ok(None)
755    }
756
757    /// Find the shortest weighted path between `from` and `to` using Dijkstra's algorithm.
758    ///
759    /// Uses `Relationship::weight` as edge cost. Negative weights are not supported
760    /// and will cause this method to return an error.
761    ///
762    /// # Returns
763    /// - `Ok(Some((path, total_weight)))` — the shortest path and its total weight
764    /// - `Ok(None)` — no path exists between `from` and `to`
765    /// - `Err(...)` — either entity not found, or a negative edge weight was encountered
766    pub fn shortest_path_weighted(
767        &self,
768        from: &EntityId,
769        to: &EntityId,
770    ) -> Result<Option<(Vec<EntityId>, f32)>, AgentRuntimeError> {
771        let inner = recover_lock(self.inner.lock(), "shortest_path_weighted");
772
773        if !inner.entities.contains_key(from) {
774            return Err(AgentRuntimeError::Graph(format!(
775                "source entity '{}' not found",
776                from.0
777            )));
778        }
779        if !inner.entities.contains_key(to) {
780            return Err(AgentRuntimeError::Graph(format!(
781                "target entity '{}' not found",
782                to.0
783            )));
784        }
785
786        // Validate: no negative weights
787        for rel in &inner.relationships {
788            if rel.weight < 0.0 {
789                return Err(AgentRuntimeError::Graph(format!(
790                    "negative weight {:.4} on edge '{}' -> '{}'",
791                    rel.weight, rel.from.0, rel.to.0
792                )));
793            }
794        }
795
796        if from == to {
797            return Ok(Some((vec![from.clone()], 0.0)));
798        }
799
800        // Dijkstra using a max-heap with negated weights to simulate a min-heap.
801        // Heap entries: (negated_cost, node_id)
802        let mut dist: HashMap<EntityId, f32> = HashMap::new();
803        let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
804        // BinaryHeap is a max-heap; negate weights to get min-heap behaviour.
805        let mut heap: BinaryHeap<(OrdF32, EntityId)> = BinaryHeap::new();
806
807        dist.insert(from.clone(), 0.0);
808        heap.push((OrdF32(-0.0), from.clone()));
809
810        while let Some((OrdF32(neg_cost), current)) = heap.pop() {
811            let cost = -neg_cost;
812
813            // Skip stale entries.
814            if let Some(&best) = dist.get(&current) {
815                if cost > best {
816                    continue;
817                }
818            }
819
820            if &current == to {
821                // Reconstruct path in reverse.
822                let mut path = vec![to.clone()];
823                let mut node = to.clone();
824                while let Some(p) = prev.get(&node) {
825                    path.push(p.clone());
826                    node = p.clone();
827                }
828                path.reverse();
829                return Ok(Some((path, cost)));
830            }
831
832            // Use the adjacency index for O(degree) neighbour lookup instead of
833            // scanning all relationships.
834            if let Some(rels) = inner.adjacency.get(&current) {
835                for rel in rels {
836                    let next_cost = cost + rel.weight;
837                    let entry = dist.entry(rel.to.clone()).or_insert(f32::INFINITY);
838                    if next_cost < *entry {
839                        *entry = next_cost;
840                        prev.insert(rel.to.clone(), current.clone());
841                        heap.push((OrdF32(-next_cost), rel.to.clone()));
842                    }
843                }
844            }
845        }
846
847        Ok(None)
848    }
849
850    /// BFS that builds a `HashSet` directly (including `start`).
851    ///
852    /// Operates on a pre-locked `GraphInner` to avoid acquiring the mutex twice
853    /// and to skip the intermediate `Vec` allocation that `bfs()` produces.
854    fn bfs_into_set(inner: &GraphInner, start: &EntityId) -> HashSet<EntityId> {
855        let mut visited: HashSet<EntityId> = HashSet::new();
856        let mut queue: VecDeque<EntityId> = VecDeque::new();
857        visited.insert(start.clone());
858        queue.push_back(start.clone());
859        while let Some(current) = queue.pop_front() {
860            for neighbour in Self::neighbours(&inner.adjacency, &current) {
861                if visited.insert(neighbour.clone()) {
862                    queue.push_back(neighbour);
863                }
864            }
865        }
866        visited
867    }
868
869    /// Compute the transitive closure: all entities reachable from `start`
870    /// (including `start` itself).
871    ///
872    /// Uses a single lock acquisition and builds the result as a `HashSet`
873    /// directly, avoiding the intermediate `Vec` that would otherwise be
874    /// produced by delegating to [`bfs`].
875    ///
876    /// [`bfs`]: GraphStore::bfs
877    pub fn transitive_closure(
878        &self,
879        start: &EntityId,
880    ) -> Result<HashSet<EntityId>, AgentRuntimeError> {
881        let inner = recover_lock(self.inner.lock(), "transitive_closure");
882        if !inner.entities.contains_key(start) {
883            return Err(AgentRuntimeError::Graph(format!(
884                "start entity '{}' not found",
885                start.0
886            )));
887        }
888        Ok(Self::bfs_into_set(&inner, start))
889    }
890
891    /// Return the number of entities in the graph.
892    pub fn entity_count(&self) -> Result<usize, AgentRuntimeError> {
893        let inner = recover_lock(self.inner.lock(), "entity_count");
894        Ok(inner.entities.len())
895    }
896
897    /// Return the number of entities in the graph.
898    ///
899    /// Alias for [`entity_count`] using graph-theory terminology.
900    ///
901    /// [`entity_count`]: GraphStore::entity_count
902    pub fn node_count(&self) -> Result<usize, AgentRuntimeError> {
903        self.entity_count()
904    }
905
906    /// Return the number of relationships in the graph.
907    pub fn relationship_count(&self) -> Result<usize, AgentRuntimeError> {
908        let inner = recover_lock(self.inner.lock(), "relationship_count");
909        Ok(inner.relationships.len())
910    }
911
912    /// Return the average out-degree across all entities.
913    ///
914    /// Returns `0.0` for an empty graph.  The result is the total number of
915    /// directed edges divided by the number of nodes.
916    pub fn average_out_degree(&self) -> Result<f64, AgentRuntimeError> {
917        let inner = recover_lock(self.inner.lock(), "average_out_degree");
918        let n = inner.entities.len();
919        if n == 0 {
920            return Ok(0.0);
921        }
922        Ok(inner.relationships.len() as f64 / n as f64)
923    }
924
925    /// Return the in-degree (number of incoming edges) for the given entity.
926    ///
927    /// Returns `0` if the entity does not exist or has no incoming edges.
928    pub fn in_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
929        let inner = recover_lock(self.inner.lock(), "in_degree_for");
930        Ok(inner
931            .reverse_adjacency
932            .get(entity_id)
933            .map(|v| v.len())
934            .unwrap_or(0))
935    }
936
937    /// Return the out-degree (number of outgoing edges) for the given entity.
938    ///
939    /// Returns `0` if the entity does not exist or has no outgoing edges.
940    pub fn out_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
941        let inner = recover_lock(self.inner.lock(), "out_degree_for");
942        Ok(inner
943            .adjacency
944            .get(entity_id)
945            .map(|v| v.len())
946            .unwrap_or(0))
947    }
948
949    /// Return all entities that have a directed edge pointing **to** `entity_id`
950    /// (its predecessors in the directed graph).
951    ///
952    /// Returns an empty `Vec` if the entity has no incoming edges or does not
953    /// exist.
954    pub fn predecessors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
955        let inner = recover_lock(self.inner.lock(), "predecessors");
956        let ids = inner
957            .reverse_adjacency
958            .get(entity_id)
959            .cloned()
960            .unwrap_or_default();
961        Ok(ids
962            .iter()
963            .filter_map(|id| inner.entities.get(id).cloned())
964            .collect())
965    }
966
967    /// Return `true` if the entity has no incoming edges (in-degree == 0).
968    ///
969    /// In a DAG, source nodes (roots) have no predecessors.  Returns `false`
970    /// if the entity does not exist.
971    pub fn is_source(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
972        Ok(self.in_degree_for(entity_id)? == 0)
973    }
974
975    /// Return all entities directly reachable from `entity_id` via outgoing edges
976    /// (its successors / immediate out-neighbors).
977    ///
978    /// Returns an empty `Vec` if the entity has no outgoing edges or does not exist.
979    pub fn successors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
980        let inner = recover_lock(self.inner.lock(), "successors");
981        let rels = inner.adjacency.get(entity_id).cloned().unwrap_or_default();
982        Ok(rels
983            .iter()
984            .filter_map(|r| inner.entities.get(&r.to).cloned())
985            .collect())
986    }
987
988    /// Return `true` if the entity has no outgoing edges (out-degree == 0).
989    ///
990    /// In a DAG, sink nodes (leaves) have no successors.  Returns `true` if
991    /// the entity does not exist (unknown nodes cannot have outgoing edges).
992    pub fn is_sink(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
993        Ok(self.out_degree_for(entity_id)? == 0)
994    }
995
996    /// Return the set of all entity IDs reachable from `start` via directed edges
997    /// (BFS traversal).  The starting node itself is **not** included.
998    ///
999    /// Returns an empty `HashSet` if `start` has no outgoing edges or does not exist.
1000    pub fn reachable_from(
1001        &self,
1002        start: &EntityId,
1003    ) -> Result<std::collections::HashSet<EntityId>, AgentRuntimeError> {
1004        let inner = recover_lock(self.inner.lock(), "reachable_from");
1005        let mut visited = std::collections::HashSet::new();
1006        let mut queue = std::collections::VecDeque::new();
1007        if let Some(rels) = inner.adjacency.get(start) {
1008            for r in rels {
1009                if visited.insert(r.to.clone()) {
1010                    queue.push_back(r.to.clone());
1011                }
1012            }
1013        }
1014        while let Some(current) = queue.pop_front() {
1015            if let Some(rels) = inner.adjacency.get(&current) {
1016                for r in rels {
1017                    if visited.insert(r.to.clone()) {
1018                        queue.push_back(r.to.clone());
1019                    }
1020                }
1021            }
1022        }
1023        Ok(visited)
1024    }
1025
1026    /// Return `true` if the directed graph contains at least one cycle.
1027    ///
1028    /// Uses iterative DFS with a three-colour scheme (unvisited / in-stack / done).
1029    /// An empty graph is acyclic.
1030    pub fn contains_cycle(&self) -> Result<bool, AgentRuntimeError> {
1031        let inner = recover_lock(self.inner.lock(), "contains_cycle");
1032        let mut color: HashMap<&EntityId, u8> = HashMap::new();
1033
1034        for start in inner.entities.keys() {
1035            if color.get(start).copied().unwrap_or(0) != 0 {
1036                continue;
1037            }
1038            let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1039            color.insert(start, 1);
1040            while let Some((node, idx)) = stack.last_mut() {
1041                let neighbors = inner.adjacency.get(node).map(|v| v.as_slice()).unwrap_or(&[]);
1042                if *idx < neighbors.len() {
1043                    let neighbor = &neighbors[*idx].to;
1044                    *idx += 1;
1045                    match color.get(neighbor).copied().unwrap_or(0) {
1046                        1 => return Ok(true),
1047                        0 => {
1048                            color.insert(neighbor, 1);
1049                            stack.push((neighbor, 0));
1050                        }
1051                        _ => {}
1052                    }
1053                } else {
1054                    color.insert(*node, 2);
1055                    stack.pop();
1056                }
1057            }
1058        }
1059        Ok(false)
1060    }
1061
1062    /// Return the number of relationships in the graph.
1063    ///
1064    /// Alias for [`relationship_count`] using graph-theory terminology.
1065    ///
1066    /// [`relationship_count`]: GraphStore::relationship_count
1067    pub fn edge_count(&self) -> Result<usize, AgentRuntimeError> {
1068        self.relationship_count()
1069    }
1070
1071    /// Return `true` if the directed graph contains **no** cycles.
1072    ///
1073    /// Shorthand for `!self.contains_cycle()`.
1074    pub fn is_acyclic(&self) -> Result<bool, AgentRuntimeError> {
1075        Ok(!self.contains_cycle()?)
1076    }
1077
1078    /// Return the mean out-degree across all entities.
1079    ///
1080    /// Returns `0.0` for graphs with no entities.
1081    pub fn avg_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1082        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_out_degree");
1083        let n = inner.entities.len();
1084        if n == 0 {
1085            return Ok(0.0);
1086        }
1087        let total: usize = inner.entities.keys().map(|id| {
1088            inner.adjacency.get(id).map_or(0, |v| v.len())
1089        }).sum();
1090        Ok(total as f64 / n as f64)
1091    }
1092
1093    /// Return the mean in-degree across all entities.
1094    ///
1095    /// Returns `0.0` for graphs with no entities.
1096    pub fn avg_in_degree(&self) -> Result<f64, AgentRuntimeError> {
1097        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_in_degree");
1098        let n = inner.entities.len();
1099        if n == 0 {
1100            return Ok(0.0);
1101        }
1102        let total: usize = inner.entities.keys().map(|id| {
1103            inner.reverse_adjacency.get(id).map_or(0, |v| v.len())
1104        }).sum();
1105        Ok(total as f64 / n as f64)
1106    }
1107
1108    /// Return the maximum out-degree across all entities, or `0` for empty graphs.
1109    pub fn max_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1110        let inner = recover_lock(self.inner.lock(), "max_out_degree");
1111        Ok(inner
1112            .adjacency
1113            .values()
1114            .map(|v| v.len())
1115            .max()
1116            .unwrap_or(0))
1117    }
1118
1119    /// Return the maximum in-degree across all entities, or `0` for empty graphs.
1120    pub fn max_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1121        let inner = recover_lock(self.inner.lock(), "max_in_degree");
1122        Ok(inner
1123            .reverse_adjacency
1124            .values()
1125            .map(|v| v.len())
1126            .max()
1127            .unwrap_or(0))
1128    }
1129
1130    /// Return `(min_weight, max_weight, mean_weight)` across all relationships.
1131    ///
1132    /// Returns `None` if the graph has no relationships.
1133    pub fn weight_stats(&self) -> Result<Option<(f64, f64, f64)>, AgentRuntimeError> {
1134        let inner = recover_lock(self.inner.lock(), "weight_stats");
1135        let weights: Vec<f64> = inner
1136            .adjacency
1137            .values()
1138            .flat_map(|rels| rels.iter())
1139            .map(|r| r.weight as f64)
1140            .collect();
1141        if weights.is_empty() {
1142            return Ok(None);
1143        }
1144        let min = weights.iter().cloned().fold(f64::INFINITY, f64::min);
1145        let max = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1146        let mean = weights.iter().sum::<f64>() / weights.len() as f64;
1147        Ok(Some((min, max, mean)))
1148    }
1149
1150    /// Return the set of entity IDs that have no inbound **and** no outbound edges.
1151    ///
1152    /// An isolated node is one that does not appear as a `from` or `to` endpoint
1153    /// in any relationship.  Useful for detecting orphaned entities.
1154    pub fn isolated_nodes(&self) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1155        let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_nodes");
1156        let mut result = HashSet::new();
1157        for id in inner.entities.keys() {
1158            let has_outbound = inner
1159                .adjacency
1160                .get(id)
1161                .map_or(false, |v| !v.is_empty());
1162            let has_inbound = inner
1163                .reverse_adjacency
1164                .get(id)
1165                .map_or(false, |v| !v.is_empty());
1166            if !has_outbound && !has_inbound {
1167                result.insert(id.clone());
1168            }
1169        }
1170        Ok(result)
1171    }
1172
1173    /// Return the sum of all relationship weights in the graph.
1174    ///
1175    /// Returns `0.0` for graphs with no relationships.
1176    pub fn sum_edge_weights(&self) -> Result<f64, AgentRuntimeError> {
1177        let inner = recover_lock(self.inner.lock(), "sum_edge_weights");
1178        Ok(inner
1179            .adjacency
1180            .values()
1181            .flat_map(|rels| rels.iter())
1182            .map(|r| r.weight as f64)
1183            .sum())
1184    }
1185
1186    /// Return all entity IDs in the graph without allocating full `Entity` objects.
1187    ///
1188    /// Cheaper than [`all_entities`] when only IDs are needed.
1189    ///
1190    /// [`all_entities`]: GraphStore::all_entities
1191    pub fn entity_ids(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1192        let inner = recover_lock(self.inner.lock(), "entity_ids");
1193        Ok(inner.entities.keys().cloned().collect())
1194    }
1195
1196    /// Return `true` if the graph contains no entities.
1197    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1198        Ok(self.entity_count()? == 0)
1199    }
1200
1201    /// Remove all entities and relationships from the graph.
1202    ///
1203    /// After this call `entity_count()` and `relationship_count()` both return `0`.
1204    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
1205        let mut inner = recover_lock(self.inner.lock(), "GraphStore::clear");
1206        inner.entities.clear();
1207        inner.relationships.clear();
1208        inner.adjacency.clear();
1209        inner.reverse_adjacency.clear();
1210        inner.cycle_cache = None;
1211        Ok(())
1212    }
1213
1214    /// Count entities whose label equals `label` (case-sensitive).
1215    pub fn entity_count_by_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
1216        let inner = recover_lock(self.inner.lock(), "entity_count_by_label");
1217        Ok(inner.entities.values().filter(|e| e.label == label).count())
1218    }
1219
1220    /// Compute the directed graph density: |E| / (|V| × (|V| − 1)).
1221    ///
1222    /// Returns `0.0` for graphs with fewer than two entities (no edges possible).
1223    /// A density of `1.0` means every possible directed edge is present.
1224    pub fn graph_density(&self) -> Result<f64, AgentRuntimeError> {
1225        let inner = recover_lock(self.inner.lock(), "graph_density");
1226        let v = inner.entities.len();
1227        if v < 2 {
1228            return Ok(0.0);
1229        }
1230        let e = inner.relationships.len() as f64;
1231        Ok(e / (v as f64 * (v - 1) as f64))
1232    }
1233
1234    /// Return all distinct entity label strings present in the graph, sorted.
1235    pub fn entity_labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
1236        let inner = recover_lock(self.inner.lock(), "entity_labels");
1237        let mut labels: Vec<String> = inner
1238            .entities
1239            .values()
1240            .map(|e| e.label.clone())
1241            .collect::<std::collections::HashSet<_>>()
1242            .into_iter()
1243            .collect();
1244        labels.sort_unstable();
1245        Ok(labels)
1246    }
1247
1248    /// Return all distinct relationship kind strings present in the graph, sorted.
1249    pub fn relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
1250        let inner = recover_lock(self.inner.lock(), "relationship_kinds");
1251        let mut kinds: Vec<String> = inner
1252            .relationships
1253            .iter()
1254            .map(|r| r.kind.clone())
1255            .collect::<std::collections::HashSet<_>>()
1256            .into_iter()
1257            .collect();
1258        kinds.sort_unstable();
1259        Ok(kinds)
1260    }
1261
1262    /// Return the number of distinct relationship kind strings present in the graph.
1263    pub fn relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
1264        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_kind_count");
1265        let count = inner
1266            .relationships
1267            .iter()
1268            .map(|r| r.kind.as_str())
1269            .collect::<std::collections::HashSet<_>>()
1270            .len();
1271        Ok(count)
1272    }
1273
1274    /// Return the `EntityId`s of entities that have at least one self-loop relationship.
1275    ///
1276    /// A self-loop is a relationship where `from == to`.
1277    pub fn entities_with_self_loops(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1278        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_self_loops");
1279        let mut ids: Vec<EntityId> = inner
1280            .adjacency
1281            .iter()
1282            .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
1283            .map(|(id, _)| id.clone())
1284            .collect();
1285        ids.sort_unstable();
1286        Ok(ids)
1287    }
1288
1289    /// Update the label of an existing entity in-place.
1290    ///
1291    /// Returns `Ok(true)` if the entity was found and updated, `Ok(false)` if not found.
1292    pub fn update_entity_label(
1293        &self,
1294        id: &EntityId,
1295        new_label: impl Into<String>,
1296    ) -> Result<bool, AgentRuntimeError> {
1297        let mut inner = recover_lock(self.inner.lock(), "update_entity_label");
1298        if let Some(entity) = inner.entities.get_mut(id) {
1299            entity.label = new_label.into();
1300            inner.cycle_cache = None;
1301            Ok(true)
1302        } else {
1303            Ok(false)
1304        }
1305    }
1306
1307    /// Compute normalized degree centrality for each entity.
1308    /// Degree = (in_degree + out_degree) / (n - 1), or 0.0 if n <= 1.
1309    pub fn degree_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1310        let inner = recover_lock(self.inner.lock(), "degree_centrality");
1311        let n = inner.entities.len();
1312
1313        // Both out-degree and in-degree are now available from the index:
1314        // adjacency[id].len()         == out-degree
1315        // reverse_adjacency[id].len() == in-degree
1316        let denom = if n <= 1 { 1.0 } else { (n - 1) as f32 };
1317        let mut result = HashMap::new();
1318        for id in inner.entities.keys() {
1319            let od = inner.adjacency.get(id).map_or(0, |v| v.len());
1320            let id_ = inner.reverse_adjacency.get(id).map_or(0, |v| v.len());
1321            let centrality = if n <= 1 {
1322                0.0
1323            } else {
1324                (od + id_) as f32 / denom
1325            };
1326            result.insert(id.clone(), centrality);
1327        }
1328
1329        Ok(result)
1330    }
1331
1332    /// Compute normalized betweenness centrality for each entity.
1333    /// Uses Brandes' algorithm with hop-count BFS.
1334    ///
1335    /// # Complexity
1336    /// O(V * E) time. Not suitable for very large graphs (>1000 nodes).
1337    pub fn betweenness_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1338        let inner = recover_lock(self.inner.lock(), "betweenness_centrality");
1339        let n = inner.entities.len();
1340        let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1341
1342        let mut centrality: HashMap<EntityId, f32> =
1343            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1344
1345        // Pre-allocate per-source work buffers and reuse them each iteration
1346        // to avoid O(V) HashMap allocations * V source nodes = O(V²) allocations.
1347        let mut stack: Vec<EntityId> = Vec::with_capacity(n);
1348        let mut predecessors: HashMap<EntityId, Vec<EntityId>> =
1349            nodes.iter().map(|id| (id.clone(), vec![])).collect();
1350        let mut sigma: HashMap<EntityId, f32> =
1351            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1352        let mut dist: HashMap<EntityId, i64> =
1353            nodes.iter().map(|id| (id.clone(), -1i64)).collect();
1354        let mut delta: HashMap<EntityId, f32> =
1355            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1356        let mut queue: VecDeque<EntityId> = VecDeque::with_capacity(n);
1357
1358        for source in &nodes {
1359            // BFS to find shortest path counts and predecessors.
1360            // Reset per-source state by clearing rather than re-allocating.
1361            stack.clear();
1362            for v in predecessors.values_mut() {
1363                v.clear();
1364            }
1365            for v in sigma.values_mut() {
1366                *v = 0.0;
1367            }
1368            for v in dist.values_mut() {
1369                *v = -1;
1370            }
1371            for v in delta.values_mut() {
1372                *v = 0.0;
1373            }
1374            queue.clear();
1375
1376            *sigma.entry(source.clone()).or_insert(0.0) = 1.0;
1377            *dist.entry(source.clone()).or_insert(-1) = 0;
1378            queue.push_back(source.clone());
1379
1380            while let Some(v) = queue.pop_front() {
1381                stack.push(v.clone());
1382                let d_v = *dist.get(&v).unwrap_or(&0);
1383                let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
1384                // Use adjacency index for O(degree) neighbour lookup.
1385                if let Some(rels) = inner.adjacency.get(&v) {
1386                    for rel in rels {
1387                        let w = &rel.to;
1388                        let d_w = dist.get(w).copied().unwrap_or(-1);
1389                        if d_w < 0 {
1390                            queue.push_back(w.clone());
1391                            *dist.entry(w.clone()).or_insert(-1) = d_v + 1;
1392                        }
1393                        if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
1394                            *sigma.entry(w.clone()).or_insert(0.0) += sigma_v;
1395                            predecessors.entry(w.clone()).or_default().push(v.clone());
1396                        }
1397                    }
1398                }
1399            }
1400
1401            // (delta map reset above at start of loop)
1402
1403            while let Some(w) = stack.pop() {
1404                let delta_w = *delta.get(&w).unwrap_or(&0.0);
1405                let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
1406                // Item 5 — iterate predecessors by reference to avoid cloning the Vec.
1407                for v in predecessors
1408                    .get(&w)
1409                    .map(|ps| ps.as_slice())
1410                    .unwrap_or_default()
1411                {
1412                    let sigma_v = *sigma.get(v).unwrap_or(&1.0);
1413                    let contribution = (sigma_v / sigma_w) * (1.0 + delta_w);
1414                    *delta.entry(v.clone()).or_insert(0.0) += contribution;
1415                }
1416                if &w != source {
1417                    *centrality.entry(w.clone()).or_insert(0.0) += delta_w;
1418                }
1419            }
1420        }
1421
1422        // Normalize by 2 / ((n-1) * (n-2)) for directed graphs.
1423        if n > 2 {
1424            let norm = 2.0 / (((n - 1) * (n - 2)) as f32);
1425            for v in centrality.values_mut() {
1426                *v *= norm;
1427            }
1428        } else {
1429            for v in centrality.values_mut() {
1430                *v = 0.0;
1431            }
1432        }
1433
1434        Ok(centrality)
1435    }
1436
1437    /// Detect communities using label propagation.
1438    /// Each entity starts as its own community. In each iteration, each entity
1439    /// adopts the most frequent label among its neighbours.
1440    /// Returns a map of entity ID → community ID (usize).
1441    pub fn label_propagation_communities(
1442        &self,
1443        max_iterations: usize,
1444    ) -> Result<HashMap<EntityId, usize>, AgentRuntimeError> {
1445        let inner = recover_lock(self.inner.lock(), "label_propagation_communities");
1446        let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1447
1448        // Build a temporary reverse-adjacency (incoming edges) so that each
1449        // node can cheaply query both its out-neighbours (via adjacency) and
1450        // its in-neighbours (via reverse_adj) without an O(|E|) scan per node.
1451        let mut reverse_adj: HashMap<EntityId, Vec<EntityId>> =
1452            nodes.iter().map(|id| (id.clone(), vec![])).collect();
1453        for rel in &inner.relationships {
1454            reverse_adj
1455                .entry(rel.to.clone())
1456                .or_default()
1457                .push(rel.from.clone());
1458        }
1459
1460        // Assign each node a unique initial label (index in nodes vec).
1461        let mut labels: HashMap<EntityId, usize> = nodes
1462            .iter()
1463            .enumerate()
1464            .map(|(i, id)| (id.clone(), i))
1465            .collect();
1466
1467        // Hoist the frequency counter out of the inner loop to avoid
1468        // allocating a fresh HashMap for every node on every iteration.
1469        let mut freq: HashMap<usize, usize> = HashMap::new();
1470
1471        for _ in 0..max_iterations {
1472            let mut changed = false;
1473            // Iterate in a stable order.
1474            for node in &nodes {
1475                // Collect neighbour labels using adjacency (out) + reverse_adj (in).
1476                let out_labels = inner
1477                    .adjacency
1478                    .get(node)
1479                    .map(|rels| {
1480                        rels.iter()
1481                            .map(|r| labels.get(&r.to).copied().unwrap_or(0))
1482                    })
1483                    .into_iter()
1484                    .flatten();
1485                let in_labels = reverse_adj
1486                    .get(node)
1487                    .map(|froms| froms.iter().map(|f| labels.get(f).copied().unwrap_or(0)))
1488                    .into_iter()
1489                    .flatten();
1490
1491                freq.clear();
1492                for lbl in out_labels.chain(in_labels) {
1493                    *freq.entry(lbl).or_insert(0) += 1;
1494                }
1495
1496                if freq.is_empty() {
1497                    continue;
1498                }
1499
1500                // Find the most frequent label.
1501                let best = freq
1502                    .iter()
1503                    .max_by_key(|&(_, count)| count)
1504                    .map(|(&lbl, _)| lbl);
1505
1506                if let Some(new_label) = best {
1507                    let current = labels.entry(node.clone()).or_insert(0);
1508                    if *current != new_label {
1509                        *current = new_label;
1510                        changed = true;
1511                    }
1512                }
1513            }
1514
1515            if !changed {
1516                break;
1517            }
1518        }
1519
1520        Ok(labels)
1521    }
1522
1523    /// Detect whether the directed graph contains any cycles.
1524    ///
1525    /// Uses iterative DFS with a three-color marking scheme.  The result is
1526    /// cached until the next mutation (`add_entity`, `add_relationship`, or
1527    /// `remove_entity`).
1528    ///
1529    /// # Returns
1530    /// - `Ok(true)` — at least one cycle exists
1531    /// - `Ok(false)` — the graph is acyclic (a DAG)
1532    pub fn detect_cycles(&self) -> Result<bool, AgentRuntimeError> {
1533        let mut inner = recover_lock(self.inner.lock(), "detect_cycles");
1534
1535        if let Some(cached) = inner.cycle_cache {
1536            return Ok(cached);
1537        }
1538
1539        // Iterative DFS with three-color marking: 0=white, 1=gray, 2=black.
1540        let mut color: HashMap<&EntityId, u8> =
1541            inner.entities.keys().map(|id| (id, 0u8)).collect();
1542
1543        let has_cycle = 'outer: {
1544            for start in inner.entities.keys() {
1545                if *color.get(start).unwrap_or(&0) != 0 {
1546                    continue;
1547                }
1548
1549                // Stack holds (node_id, iterator position for adjacency).
1550                let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1551                *color.entry(start).or_insert(0) = 1;
1552
1553                while let Some((node, idx)) = stack.last_mut() {
1554                    // Use the adjacency index (O(1) lookup) instead of an
1555                    // O(|E|) relationship scan on every while-loop step.
1556                    let rels = inner
1557                        .adjacency
1558                        .get(*node)
1559                        .map(|v| v.as_slice())
1560                        .unwrap_or(&[]);
1561
1562                    if *idx < rels.len() {
1563                        let next = &rels[*idx].to;
1564                        *idx += 1;
1565                        match color.get(next).copied().unwrap_or(0) {
1566                            1 => break 'outer true, // back edge → cycle
1567                            0 => {
1568                                *color.entry(next).or_insert(0) = 1;
1569                                stack.push((next, 0));
1570                            }
1571                            _ => {} // already fully processed
1572                        }
1573                    } else {
1574                        // All neighbors processed; color black.
1575                        *color.entry(*node).or_insert(0) = 2;
1576                        stack.pop();
1577                    }
1578                }
1579            }
1580            false
1581        };
1582
1583        inner.cycle_cache = Some(has_cycle);
1584        Ok(has_cycle)
1585    }
1586
1587    /// Compute a topological ordering of all entities.
1588    ///
1589    /// Returns the entities sorted so that for every directed edge `(A → B)`,
1590    /// `A` appears before `B` in the result.  Uses iterative DFS (post-order).
1591    ///
1592    /// # Errors
1593    /// Returns `Err(AgentRuntimeError::Graph(...))` if the graph contains a cycle.
1594    pub fn topological_sort(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1595        let inner = recover_lock(self.inner.lock(), "topological_sort");
1596
1597        // Three-color DFS: 0=white, 1=gray (in-stack), 2=black (done).
1598        let mut color: HashMap<&EntityId, u8> =
1599            inner.entities.keys().map(|id| (id, 0u8)).collect();
1600        let mut result: Vec<EntityId> = Vec::with_capacity(inner.entities.len());
1601
1602        for start in inner.entities.keys() {
1603            if *color.get(start).unwrap_or(&0) != 0 {
1604                continue;
1605            }
1606            // Stack: (node, adjacency_index, already_pushed_post_order)
1607            let mut stack: Vec<(&EntityId, usize, bool)> = vec![(start, 0, false)];
1608            *color.entry(start).or_insert(0) = 1;
1609
1610            while let Some((node, idx, pushed)) = stack.last_mut() {
1611                let rels = inner.adjacency.get(*node).map(|v| v.as_slice()).unwrap_or(&[]);
1612                if *idx < rels.len() {
1613                    let next = &rels[*idx].to;
1614                    *idx += 1;
1615                    match color.get(next).copied().unwrap_or(0) {
1616                        1 => return Err(AgentRuntimeError::Graph(
1617                            "topological_sort: graph contains a cycle".into(),
1618                        )),
1619                        0 => {
1620                            *color.entry(next).or_insert(0) = 1;
1621                            stack.push((next, 0, false));
1622                        }
1623                        _ => {} // already finished
1624                    }
1625                } else {
1626                    if !*pushed {
1627                        result.push((*node).clone());
1628                        *pushed = true;
1629                    }
1630                    *color.entry(*node).or_insert(0) = 2;
1631                    stack.pop();
1632                }
1633            }
1634        }
1635
1636        result.reverse();
1637        Ok(result)
1638    }
1639
1640    /// Update a single property on an existing entity.
1641    ///
1642    /// Returns `Ok(true)` if the entity was found and updated, `Ok(false)` otherwise.
1643    pub fn update_entity_property(
1644        &self,
1645        id: &EntityId,
1646        key: impl Into<String>,
1647        value: serde_json::Value,
1648    ) -> Result<bool, AgentRuntimeError> {
1649        let mut inner = recover_lock(self.inner.lock(), "update_entity_property");
1650        if let Some(entity) = inner.entities.get_mut(id) {
1651            entity.properties.insert(key.into(), value);
1652            Ok(true)
1653        } else {
1654            Ok(false)
1655        }
1656    }
1657
1658    /// Return `true` if the graph is a DAG (directed acyclic graph).
1659    ///
1660    /// Equivalent to `!detect_cycles()` but reads more naturally in
1661    /// condition expressions.
1662    pub fn is_dag(&self) -> Result<bool, AgentRuntimeError> {
1663        Ok(!self.detect_cycles()?)
1664    }
1665
1666    /// Count the number of weakly connected components in the graph.
1667    ///
1668    /// Treats all edges as undirected for component assignment.  Isolated
1669    /// nodes (no edges) each count as their own component.  Uses Union-Find.
1670    pub fn connected_components(&self) -> Result<usize, AgentRuntimeError> {
1671        let inner = recover_lock(self.inner.lock(), "connected_components");
1672        let ids: Vec<&EntityId> = inner.entities.keys().collect();
1673        if ids.is_empty() {
1674            return Ok(0);
1675        }
1676
1677        // Map EntityId → index for Union-Find.
1678        let idx: HashMap<&EntityId, usize> =
1679            ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
1680        let mut parent: Vec<usize> = (0..ids.len()).collect();
1681
1682        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1683            if parent[x] != x {
1684                parent[x] = find(parent, parent[x]);
1685            }
1686            parent[x]
1687        }
1688
1689        fn union(parent: &mut Vec<usize>, a: usize, b: usize) {
1690            let ra = find(parent, a);
1691            let rb = find(parent, b);
1692            if ra != rb {
1693                parent[ra] = rb;
1694            }
1695        }
1696
1697        for rel in &inner.relationships {
1698            if let (Some(&a), Some(&b)) = (idx.get(&rel.from), idx.get(&rel.to)) {
1699                union(&mut parent, a, b);
1700            }
1701        }
1702
1703        let components = ids
1704            .iter()
1705            .enumerate()
1706            .filter(|(i, _)| find(&mut parent, *i) == *i)
1707            .count();
1708        Ok(components)
1709    }
1710
1711    /// Return `true` if the graph is weakly connected (i.e. has at most one
1712    /// connected component when all edges are treated as undirected).
1713    ///
1714    /// An empty graph is considered weakly connected by convention.
1715    ///
1716    /// # Errors
1717    /// Propagates any lock-poisoning error from [`connected_components`].
1718    pub fn weakly_connected(&self) -> Result<bool, AgentRuntimeError> {
1719        Ok(self.connected_components()? <= 1)
1720    }
1721
1722    /// Return all entities that have no outgoing edges (out-degree == 0).
1723    ///
1724    /// In a DAG these are the leaf nodes. Useful for identifying terminal
1725    /// states in a workflow or dependency graph.
1726    pub fn sink_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1727        let inner = recover_lock(self.inner.lock(), "sink_nodes");
1728        // Use adjacency index: entities with no entry (or an empty entry) have no outgoing edges.
1729        Ok(inner
1730            .entities
1731            .values()
1732            .filter(|e| {
1733                inner
1734                    .adjacency
1735                    .get(&e.id)
1736                    .map_or(true, |v| v.is_empty())
1737            })
1738            .cloned()
1739            .collect())
1740    }
1741
1742    /// Return all entities that have no incoming edges (in-degree == 0).
1743    ///
1744    /// In a DAG these are the root nodes. Useful for finding entry points in
1745    /// a workflow or dependency graph.
1746    pub fn source_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1747        let inner = recover_lock(self.inner.lock(), "source_nodes");
1748        // Use reverse_adjacency: entities with no entry (or an empty entry) have no incoming edges.
1749        Ok(inner
1750            .entities
1751            .values()
1752            .filter(|e| {
1753                inner
1754                    .reverse_adjacency
1755                    .get(&e.id)
1756                    .map_or(true, |v| v.is_empty())
1757            })
1758            .cloned()
1759            .collect())
1760    }
1761
1762    /// Return all entities that have no edges (neither incoming nor outgoing).
1763    pub fn isolates(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1764        let inner = recover_lock(self.inner.lock(), "isolates");
1765        // An entity is isolated if it has no outgoing edges (adjacency) and no
1766        // incoming edges (reverse_adjacency).
1767        Ok(inner
1768            .entities
1769            .values()
1770            .filter(|e| {
1771                inner
1772                    .adjacency
1773                    .get(&e.id)
1774                    .map_or(true, |v| v.is_empty())
1775                    && inner
1776                        .reverse_adjacency
1777                        .get(&e.id)
1778                        .map_or(true, |v| v.is_empty())
1779            })
1780            .cloned()
1781            .collect())
1782    }
1783
1784    /// Return the number of outgoing edges (out-degree) for `id`.
1785    ///
1786    /// Returns `0` if the entity has no outgoing edges or does not exist.
1787    pub fn out_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1788        let inner = recover_lock(self.inner.lock(), "out_degree");
1789        Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
1790    }
1791
1792    /// Return the number of incoming edges (in-degree) for `id`.
1793    ///
1794    /// Uses the reverse adjacency index for O(in-degree) lookup.
1795    pub fn in_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1796        let inner = recover_lock(self.inner.lock(), "in_degree");
1797        Ok(inner
1798            .reverse_adjacency
1799            .get(id)
1800            .map_or(0, |srcs| srcs.len()))
1801    }
1802
1803    /// Return `true` if there is any path from `from` to `to`.
1804    ///
1805    /// Both nodes must exist or returns `Err`. Uses BFS internally.
1806    /// Returns `Ok(false)` if nodes exist but are not connected.
1807    pub fn path_exists(&self, from: &str, to: &str) -> Result<bool, AgentRuntimeError> {
1808        let from_id = EntityId::new(from);
1809        let to_id = EntityId::new(to);
1810        match self.shortest_path(&from_id, &to_id) {
1811            Ok(Some(_)) => Ok(true),
1812            Ok(None) => Ok(false),
1813            Err(e) => Err(e),
1814        }
1815    }
1816
1817    /// Return the hop-count of the shortest path from `from` to `to`.
1818    ///
1819    /// Returns `Some(hops)` if a path exists, `None` if the nodes are
1820    /// disconnected.  Both node IDs must exist or returns `Err`.
1821    pub fn shortest_path_length(
1822        &self,
1823        from: &EntityId,
1824        to: &EntityId,
1825    ) -> Result<Option<usize>, AgentRuntimeError> {
1826        Ok(self.shortest_path(from, to)?.map(|path| path.len().saturating_sub(1)))
1827    }
1828
1829    /// BFS limited by maximum depth and maximum node count.
1830    ///
1831    /// Returns the subset of nodes visited within those limits (including start).
1832    pub fn bfs_bounded(
1833        &self,
1834        start: &str,
1835        max_depth: usize,
1836        max_nodes: usize,
1837    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
1838        let inner = recover_lock(self.inner.lock(), "bfs_bounded");
1839        let start_id = EntityId::new(start);
1840        if !inner.entities.contains_key(&start_id) {
1841            return Err(AgentRuntimeError::Graph(format!(
1842                "start entity '{start}' not found"
1843            )));
1844        }
1845
1846        let mut visited: std::collections::HashMap<EntityId, usize> = std::collections::HashMap::new();
1847        let mut queue: VecDeque<(EntityId, usize)> = VecDeque::new();
1848        let mut result: Vec<EntityId> = Vec::new();
1849
1850        visited.insert(start_id.clone(), 0);
1851        queue.push_back((start_id.clone(), 0));
1852        result.push(start_id);
1853
1854        while let Some((current, depth)) = queue.pop_front() {
1855            if result.len() >= max_nodes {
1856                break;
1857            }
1858            if depth >= max_depth {
1859                continue;
1860            }
1861            for neighbour in Self::neighbours(&inner.adjacency, &current) {
1862                if !visited.contains_key(&neighbour) {
1863                    let new_depth = depth + 1;
1864                    visited.insert(neighbour.clone(), new_depth);
1865                    result.push(neighbour.clone());
1866                    if result.len() >= max_nodes {
1867                        break;
1868                    }
1869                    queue.push_back((neighbour, new_depth));
1870                }
1871            }
1872        }
1873
1874        Ok(result)
1875    }
1876
1877    /// DFS limited by maximum depth and maximum node count.
1878    ///
1879    /// Returns the subset of nodes visited within those limits (including start).
1880    pub fn dfs_bounded(
1881        &self,
1882        start: &str,
1883        max_depth: usize,
1884        max_nodes: usize,
1885    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
1886        let inner = recover_lock(self.inner.lock(), "dfs_bounded");
1887        let start_id = EntityId::new(start);
1888        if !inner.entities.contains_key(&start_id) {
1889            return Err(AgentRuntimeError::Graph(format!(
1890                "start entity '{start}' not found"
1891            )));
1892        }
1893
1894        let mut visited: HashSet<EntityId> = HashSet::new();
1895        let mut stack: Vec<(EntityId, usize)> = Vec::new();
1896        let mut result: Vec<EntityId> = Vec::new();
1897
1898        visited.insert(start_id.clone());
1899        stack.push((start_id.clone(), 0));
1900        result.push(start_id);
1901
1902        while let Some((current, depth)) = stack.pop() {
1903            if result.len() >= max_nodes {
1904                break;
1905            }
1906            if depth >= max_depth {
1907                continue;
1908            }
1909            for neighbour in Self::neighbours(&inner.adjacency, &current) {
1910                if visited.insert(neighbour.clone()) {
1911                    result.push(neighbour.clone());
1912                    if result.len() >= max_nodes {
1913                        break;
1914                    }
1915                    stack.push((neighbour, depth + 1));
1916                }
1917            }
1918        }
1919
1920        Ok(result)
1921    }
1922
1923    /// Return all entities in the graph.
1924    pub fn all_entities(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1925        let inner = recover_lock(self.inner.lock(), "all_entities");
1926        Ok(inner.entities.values().cloned().collect())
1927    }
1928
1929    /// Return all relationships in the graph.
1930    pub fn all_relationships(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
1931        let inner = recover_lock(self.inner.lock(), "all_relationships");
1932        Ok(inner.relationships.clone())
1933    }
1934
1935    /// Return all relationships whose `kind` matches `kind` (case-sensitive).
1936    pub fn find_relationships_by_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
1937        let inner = recover_lock(self.inner.lock(), "find_relationships_by_kind");
1938        Ok(inner.relationships.iter().filter(|r| r.kind == kind).cloned().collect())
1939    }
1940
1941    /// Return the number of relationships whose `kind` matches `kind` (case-sensitive).
1942    pub fn count_relationships_by_kind(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
1943        let inner = recover_lock(self.inner.lock(), "count_relationships_by_kind");
1944        Ok(inner.relationships.iter().filter(|r| r.kind == kind).count())
1945    }
1946
1947    /// Return all entities whose `label` matches `label` (case-sensitive).
1948    pub fn find_entities_by_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
1949        let inner = recover_lock(self.inner.lock(), "find_entities_by_label");
1950        Ok(inner
1951            .entities
1952            .values()
1953            .filter(|e| e.label == label)
1954            .cloned()
1955            .collect())
1956    }
1957
1958    /// Return all entities whose label is contained in `labels`.
1959    ///
1960    /// Order of results is unspecified.  An empty `labels` slice returns an
1961    /// empty result (never all entities).
1962    pub fn find_entities_by_labels(&self, labels: &[&str]) -> Result<Vec<Entity>, AgentRuntimeError> {
1963        let inner = recover_lock(self.inner.lock(), "find_entities_by_labels");
1964        let label_set: std::collections::HashSet<&str> = labels.iter().copied().collect();
1965        Ok(inner
1966            .entities
1967            .values()
1968            .filter(|e| label_set.contains(e.label.as_str()))
1969            .cloned()
1970            .collect())
1971    }
1972
1973    /// Remove all isolated entities (those with no incoming or outgoing edges).
1974    ///
1975    /// Returns the number of entities removed.
1976    pub fn remove_isolated(&self) -> Result<usize, AgentRuntimeError> {
1977        let mut inner = recover_lock(self.inner.lock(), "remove_isolated");
1978        let isolated: Vec<EntityId> = inner
1979            .entities
1980            .keys()
1981            .filter(|id| {
1982                inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
1983                    && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
1984            })
1985            .cloned()
1986            .collect();
1987        let count = isolated.len();
1988        for id in &isolated {
1989            inner.entities.remove(id);
1990            inner.adjacency.remove(id);
1991            inner.reverse_adjacency.remove(id);
1992        }
1993        if count > 0 {
1994            inner.cycle_cache = None;
1995        }
1996        Ok(count)
1997    }
1998
1999    /// Return all outgoing relationships from `id` (those where `id` is the source).
2000    pub fn get_relationships_for(
2001        &self,
2002        id: &EntityId,
2003    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2004        let inner = recover_lock(self.inner.lock(), "get_relationships_for");
2005        Ok(inner
2006            .adjacency
2007            .get(id)
2008            .cloned()
2009            .unwrap_or_default())
2010    }
2011
2012    /// Return all relationships where both endpoints are `from` and `to` (any direction or kind).
2013    pub fn relationships_between(
2014        &self,
2015        from: &EntityId,
2016        to: &EntityId,
2017    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2018        let inner = recover_lock(self.inner.lock(), "relationships_between");
2019        Ok(inner
2020            .relationships
2021            .iter()
2022            .filter(|r| {
2023                (r.from == *from && r.to == *to) || (r.from == *to && r.to == *from)
2024            })
2025            .cloned()
2026            .collect())
2027    }
2028
2029    /// Find entities that have a property `key` whose value equals `expected`.
2030    ///
2031    /// Uses `serde_json::Value` equality; for string properties pass
2032    /// `serde_json::Value::String(...)`.
2033    pub fn find_entities_by_property(
2034        &self,
2035        key: &str,
2036        expected: &serde_json::Value,
2037    ) -> Result<Vec<Entity>, AgentRuntimeError> {
2038        let inner = recover_lock(self.inner.lock(), "find_entities_by_property");
2039        Ok(inner
2040            .entities
2041            .values()
2042            .filter(|e| e.properties.get(key) == Some(expected))
2043            .cloned()
2044            .collect())
2045    }
2046
2047    /// Merge another `GraphStore` into this one.
2048    ///
2049    /// Entities are inserted (or replaced if the same ID exists); relationships
2050    /// are inserted only if the `(from, to, kind)` triple does not already exist.
2051    pub fn merge(&self, other: &GraphStore) -> Result<(), AgentRuntimeError> {
2052        let other_inner = recover_lock(other.inner.lock(), "merge:read");
2053        let other_entities: Vec<Entity> = other_inner.entities.values().cloned().collect();
2054        let other_rels: Vec<Relationship> = other_inner.relationships.clone();
2055        drop(other_inner);
2056
2057        let mut inner = recover_lock(self.inner.lock(), "merge:write");
2058        inner.cycle_cache = None;
2059        for entity in other_entities {
2060            inner.adjacency.entry(entity.id.clone()).or_default();
2061            inner.entities.insert(entity.id.clone(), entity);
2062        }
2063        for rel in other_rels {
2064            let already_exists = inner
2065                .relationships
2066                .iter()
2067                .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
2068            if !already_exists && inner.entities.contains_key(&rel.from) && inner.entities.contains_key(&rel.to) {
2069                inner
2070                    .adjacency
2071                    .entry(rel.from.clone())
2072                    .or_default()
2073                    .push(rel.clone());
2074                inner.relationships.push(rel);
2075            }
2076        }
2077        Ok(())
2078    }
2079
2080    /// Return the `Entity` objects for all direct out-neighbors of `id`.
2081    ///
2082    /// Neighbors are the targets of all outgoing relationships from `id`.
2083    /// Entities that no longer exist (orphan edge targets) are silently skipped.
2084    pub fn neighbor_entities(&self, id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
2085        let inner = recover_lock(self.inner.lock(), "neighbor_entities");
2086        let neighbors: Vec<Entity> = inner
2087            .adjacency
2088            .get(id)
2089            .iter()
2090            .flat_map(|rels| rels.iter())
2091            .filter_map(|r| inner.entities.get(&r.to).cloned())
2092            .collect();
2093        Ok(neighbors)
2094    }
2095
2096    /// Return the IDs of all directly reachable neighbours from `id`
2097    /// (i.e. the `to` end of every outgoing relationship).
2098    ///
2099    /// Cheaper than [`neighbor_entities`] when only IDs are needed.
2100    ///
2101    /// [`neighbor_entities`]: GraphStore::neighbor_entities
2102    pub fn neighbor_ids(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2103        let inner = recover_lock(self.inner.lock(), "neighbor_ids");
2104        let ids: Vec<EntityId> = inner
2105            .adjacency
2106            .get(id)
2107            .iter()
2108            .flat_map(|rels| rels.iter())
2109            .map(|r| r.to.clone())
2110            .collect();
2111        Ok(ids)
2112    }
2113
2114    /// Remove **all** relationships where `id` is the source (outgoing edges).
2115    ///
2116    /// Also updates the adjacency index.  Does **not** remove incoming edges
2117    /// from other nodes that point to `id` — use this before [`remove_entity`]
2118    /// to ensure consistent graph state.
2119    ///
2120    /// Returns the number of relationships removed.
2121    ///
2122    /// [`remove_entity`]: GraphStore::remove_entity
2123    pub fn remove_all_relationships_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
2124        let mut inner = recover_lock(self.inner.lock(), "remove_all_relationships_for");
2125        inner.adjacency.remove(id);
2126        let before = inner.relationships.len();
2127        inner.relationships.retain(|r| &r.from != id);
2128        inner.cycle_cache = None;
2129        Ok(before - inner.relationships.len())
2130    }
2131
2132    /// Check whether an entity with the given ID exists.
2133    pub fn entity_exists(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
2134        let inner = recover_lock(self.inner.lock(), "entity_exists");
2135        Ok(inner.entities.contains_key(id))
2136    }
2137
2138    /// Check whether a relationship `(from, to, kind)` exists.
2139    ///
2140    /// Uses the adjacency index (O(out-degree of `from`)) rather than a full
2141    /// O(|E|) scan over all relationships.
2142    pub fn relationship_exists(
2143        &self,
2144        from: &EntityId,
2145        to: &EntityId,
2146        kind: &str,
2147    ) -> Result<bool, AgentRuntimeError> {
2148        let inner = recover_lock(self.inner.lock(), "relationship_exists");
2149        Ok(inner
2150            .adjacency
2151            .get(from)
2152            .map_or(false, |rels| rels.iter().any(|r| r.to == *to && r.kind == kind)))
2153    }
2154
2155    /// Extract a subgraph containing only the specified entities and the
2156    /// relationships between them.
2157    pub fn subgraph(&self, node_ids: &[EntityId]) -> Result<GraphStore, AgentRuntimeError> {
2158        let inner = recover_lock(self.inner.lock(), "subgraph");
2159        let id_set: HashSet<&EntityId> = node_ids.iter().collect();
2160
2161        let new_store = GraphStore::new();
2162
2163        // Validate all entities exist before mutating the new store, so we
2164        // don't end up with a partially-populated subgraph on error.
2165        let entities_to_copy: Vec<Entity> = node_ids
2166            .iter()
2167            .map(|id| {
2168                inner
2169                    .entities
2170                    .get(id)
2171                    .cloned()
2172                    .ok_or_else(|| {
2173                        AgentRuntimeError::Graph(format!("entity '{}' not found", id.0))
2174                    })
2175            })
2176            .collect::<Result<_, _>>()?;
2177
2178        // Acquire the new store's lock once for all entity insertions.
2179        {
2180            let mut new_inner = recover_lock(new_store.inner.lock(), "subgraph:add_entities");
2181            for entity in entities_to_copy {
2182                // Ensure an adjacency entry exists for this entity.
2183                new_inner.adjacency.entry(entity.id.clone()).or_default();
2184                new_inner.entities.insert(entity.id.clone(), entity);
2185            }
2186        }
2187
2188        // Acquire the new store's lock once for the entire relationship batch
2189        // rather than once per relationship.
2190        {
2191            let mut new_inner =
2192                recover_lock(new_store.inner.lock(), "subgraph:add_relationships");
2193            for rel in inner.relationships.iter() {
2194                if id_set.contains(&rel.from) && id_set.contains(&rel.to) {
2195                    // Keep adjacency index in sync with relationships.
2196                    new_inner
2197                        .adjacency
2198                        .entry(rel.from.clone())
2199                        .or_default()
2200                        .push(rel.clone());
2201                    new_inner.relationships.push(rel.clone());
2202                }
2203            }
2204        }
2205
2206        Ok(new_store)
2207    }
2208
2209    /// Return a new graph with all edge directions reversed.
2210    ///
2211    /// Every relationship `A → B` becomes `B → A` in the returned graph.
2212    /// All entities are copied; relationship weights and kinds are preserved.
2213    pub fn reverse(&self) -> Result<GraphStore, AgentRuntimeError> {
2214        let inner = recover_lock(self.inner.lock(), "reverse");
2215        let reversed = GraphStore::new();
2216
2217        // Copy all entities.
2218        {
2219            let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:entities");
2220            for entity in inner.entities.values() {
2221                r_inner.adjacency.entry(entity.id.clone()).or_default();
2222                r_inner.entities.insert(entity.id.clone(), entity.clone());
2223            }
2224        }
2225
2226        // Add reversed relationships.
2227        for rel in &inner.relationships {
2228            let flipped = Relationship {
2229                from: rel.to.clone(),
2230                to: rel.from.clone(),
2231                kind: rel.kind.clone(),
2232                weight: rel.weight,
2233            };
2234            let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:rels");
2235            r_inner
2236                .adjacency
2237                .entry(flipped.from.clone())
2238                .or_default()
2239                .push(flipped.clone());
2240            r_inner.relationships.push(flipped);
2241        }
2242
2243        Ok(reversed)
2244    }
2245
2246    /// Return entities that are out-neighbors of **both** `a` and `b`.
2247    ///
2248    /// Useful for link-prediction and community detection heuristics.
2249    /// Returns an empty `Vec` if either node has no outgoing edges.
2250    pub fn common_neighbors(
2251        &self,
2252        a: &EntityId,
2253        b: &EntityId,
2254    ) -> Result<Vec<Entity>, AgentRuntimeError> {
2255        let inner = recover_lock(self.inner.lock(), "common_neighbors");
2256        let a_set: HashSet<&EntityId> = inner
2257            .adjacency
2258            .get(a)
2259            .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2260        let b_set: HashSet<&EntityId> = inner
2261            .adjacency
2262            .get(b)
2263            .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2264        let common: Vec<Entity> = a_set
2265            .intersection(&b_set)
2266            .filter_map(|id| inner.entities.get(*id).cloned())
2267            .collect();
2268        Ok(common)
2269    }
2270
2271    /// Return the weight of the first relationship from `from` to `to`.
2272    ///
2273    /// Returns `None` if no such edge exists.
2274    pub fn weight_of(
2275        &self,
2276        from: &EntityId,
2277        to: &EntityId,
2278    ) -> Result<Option<f32>, AgentRuntimeError> {
2279        let inner = recover_lock(self.inner.lock(), "weight_of");
2280        let weight = inner
2281            .adjacency
2282            .get(from)
2283            .and_then(|rels| rels.iter().find(|r| &r.to == to))
2284            .map(|r| r.weight);
2285        Ok(weight)
2286    }
2287
2288    /// Return the IDs of all entities that have an outgoing edge pointing **to** `id`.
2289    ///
2290    /// This is the inverse of `neighbor_entities`, which returns out-neighbors.
2291    /// Returns an empty `Vec` if no incoming edges exist for `id`.
2292    pub fn neighbors_in(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2293        let inner = recover_lock(self.inner.lock(), "neighbors_in");
2294        // Use reverse_adjacency for O(in-degree) lookup instead of O(|E|) scan.
2295        Ok(inner
2296            .reverse_adjacency
2297            .get(id)
2298            .cloned()
2299            .unwrap_or_default())
2300    }
2301
2302    /// Return the graph density: `edges / (nodes * (nodes − 1))` for a directed graph.
2303    ///
2304    /// Returns `0.0` when the graph has fewer than 2 nodes (no directed edges are
2305    /// possible). Values range from `0.0` (sparse) to `1.0` (complete).
2306    pub fn density(&self) -> Result<f64, AgentRuntimeError> {
2307        let inner = recover_lock(self.inner.lock(), "density");
2308        let n = inner.entities.len();
2309        if n < 2 {
2310            return Ok(0.0);
2311        }
2312        let max_edges = n * (n - 1);
2313        Ok(inner.relationships.len() as f64 / max_edges as f64)
2314    }
2315
2316    /// Return the average out-degree across all nodes.
2317    ///
2318    /// Returns `0.0` for an empty graph.
2319    pub fn avg_degree(&self) -> Result<f64, AgentRuntimeError> {
2320        let inner = recover_lock(self.inner.lock(), "avg_degree");
2321        let n = inner.entities.len();
2322        if n == 0 {
2323            return Ok(0.0);
2324        }
2325        Ok(inner.relationships.len() as f64 / n as f64)
2326    }
2327
2328    /// Return the sum of all edge weights in the graph.
2329    ///
2330    /// Returns `0.0` for an empty graph or a graph with no relationships.
2331    pub fn total_weight(&self) -> Result<f32, AgentRuntimeError> {
2332        let inner = recover_lock(self.inner.lock(), "total_weight");
2333        Ok(inner.relationships.iter().map(|r| r.weight).sum())
2334    }
2335
2336    /// Return the maximum edge weight, or `None` if the graph has no edges.
2337    pub fn max_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2338        let inner = recover_lock(self.inner.lock(), "max_edge_weight");
2339        Ok(inner
2340            .relationships
2341            .iter()
2342            .map(|r| r.weight)
2343            .reduce(f32::max))
2344    }
2345
2346    /// Return the minimum edge weight, or `None` if the graph has no edges.
2347    pub fn min_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2348        let inner = recover_lock(self.inner.lock(), "min_edge_weight");
2349        Ok(inner
2350            .relationships
2351            .iter()
2352            .map(|r| r.weight)
2353            .reduce(f32::min))
2354    }
2355
2356    /// Return the top-N entities by out-degree (most outgoing edges first).
2357    ///
2358    /// If `n == 0` or the graph is empty, returns an empty Vec.  Ties are
2359    /// broken by arbitrary hash-map iteration order.
2360    pub fn top_n_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2361        if n == 0 {
2362            return Ok(Vec::new());
2363        }
2364        let inner = recover_lock(self.inner.lock(), "top_n_by_out_degree");
2365        let mut pairs: Vec<(&EntityId, usize)> = inner
2366            .adjacency
2367            .iter()
2368            .map(|(id, rels)| (id, rels.len()))
2369            .collect();
2370        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2371        Ok(pairs
2372            .into_iter()
2373            .take(n)
2374            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2375            .collect())
2376    }
2377
2378    /// Remove `id` and all relationships where it is the source or target.
2379    ///
2380    /// Equivalent to calling `remove_entity` and `remove_all_relationships_for`
2381    /// in a single lock acquisition, avoiding two separate look-ups.
2382    ///
2383    /// Returns `Ok(())` if the entity was found and removed.  Returns
2384    /// `Err(AgentRuntimeError::Graph)` if no entity with that ID exists.
2385    pub fn remove_entity_and_edges(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
2386        let mut inner = recover_lock(self.inner.lock(), "remove_entity_and_edges");
2387        if !inner.entities.contains_key(id) {
2388            return Err(AgentRuntimeError::Graph(format!(
2389                "entity '{}' not found",
2390                id.0
2391            )));
2392        }
2393        inner.entities.remove(id);
2394        inner.relationships.retain(|r| &r.from != id && &r.to != id);
2395        inner.adjacency.remove(id);
2396        for adj in inner.adjacency.values_mut() {
2397            adj.retain(|r| &r.to != id);
2398        }
2399        inner.reverse_adjacency.remove(id);
2400        for rev in inner.reverse_adjacency.values_mut() {
2401            rev.retain(|src| src != id);
2402        }
2403        inner.cycle_cache = None;
2404        Ok(())
2405    }
2406
2407    /// Return all entities whose out-degree is at or above `threshold`.
2408    ///
2409    /// Useful for finding hub or gateway nodes in a knowledge graph.
2410    pub fn hub_nodes(&self, threshold: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2411        let inner = recover_lock(self.inner.lock(), "hub_nodes");
2412        Ok(inner
2413            .entities
2414            .values()
2415            .filter(|e| {
2416                inner
2417                    .adjacency
2418                    .get(&e.id)
2419                    .map_or(0, |rels| rels.len())
2420                    >= threshold
2421            })
2422            .cloned()
2423            .collect())
2424    }
2425
2426    /// Return all relationships incident to `entity_id`
2427    /// (where the entity is either the source or the target).
2428    pub fn incident_relationships(
2429        &self,
2430        entity_id: &EntityId,
2431    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2432        let inner = recover_lock(self.inner.lock(), "incident_relationships");
2433        Ok(inner
2434            .relationships
2435            .iter()
2436            .filter(|r| &r.from == entity_id || &r.to == entity_id)
2437            .cloned()
2438            .collect())
2439    }
2440
2441    /// Return the entity with the highest out-degree (most outgoing edges),
2442    /// or `None` if the graph is empty.
2443    ///
2444    /// If multiple entities share the maximum out-degree, the first one
2445    /// encountered (in arbitrary hash-map iteration order) is returned.
2446    pub fn max_out_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2447        let inner = recover_lock(self.inner.lock(), "max_out_degree_entity");
2448        let best = inner
2449            .adjacency
2450            .iter()
2451            .max_by_key(|(_, rels)| rels.len())
2452            .and_then(|(id, _)| inner.entities.get(id).cloned());
2453        Ok(best)
2454    }
2455
2456    /// Return the entity with the most incoming edges (highest in-degree).
2457    ///
2458    /// Uses the reverse adjacency index for O(V) computation.
2459    /// Returns `None` for an empty graph or a graph with no edges.
2460    pub fn max_in_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2461        let inner = recover_lock(self.inner.lock(), "max_in_degree_entity");
2462        let best = inner
2463            .reverse_adjacency
2464            .iter()
2465            .max_by_key(|(_, srcs)| srcs.len())
2466            .and_then(|(id, _)| inner.entities.get(id).cloned());
2467        Ok(best)
2468    }
2469
2470    /// Return all entities with out-degree = 0 (no outgoing edges).
2471    ///
2472    /// Leaf nodes (also called sink nodes in directed graphs) are entities
2473    /// that have no outgoing relationships.  See also [`sink_nodes`] which
2474    /// also counts nodes with no outgoing edges but filters differently.
2475    ///
2476    /// [`sink_nodes`]: GraphStore::sink_nodes
2477    pub fn leaf_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2478        let inner = recover_lock(self.inner.lock(), "leaf_nodes");
2479        Ok(inner
2480            .entities
2481            .values()
2482            .filter(|e| {
2483                inner
2484                    .adjacency
2485                    .get(&e.id)
2486                    .map_or(true, |rels| rels.is_empty())
2487            })
2488            .cloned()
2489            .collect())
2490    }
2491
2492    /// Return the top `n` entities sorted by out-degree (descending).
2493    ///
2494    /// Uses the adjacency index for O(V log V) computation.  Useful for finding
2495    /// hub nodes — entities with the most outgoing connections.
2496    /// Returns fewer than `n` entities if the graph has fewer nodes.
2497    pub fn top_nodes_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2498        let inner = recover_lock(self.inner.lock(), "top_nodes_by_out_degree");
2499        let mut pairs: Vec<(&EntityId, usize)> = inner
2500            .entities
2501            .keys()
2502            .map(|id| (id, inner.adjacency.get(id).map_or(0, |v| v.len())))
2503            .collect();
2504        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2505        pairs.truncate(n);
2506        Ok(pairs
2507            .into_iter()
2508            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2509            .collect())
2510    }
2511
2512    /// Return the top `n` entities sorted by in-degree (descending).
2513    ///
2514    /// Uses the reverse adjacency index for O(V log V) computation.  Useful for
2515    /// finding sink hubs — entities with the most incoming connections.
2516    pub fn top_nodes_by_in_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2517        let inner = recover_lock(self.inner.lock(), "top_nodes_by_in_degree");
2518        let mut pairs: Vec<(&EntityId, usize)> = inner
2519            .entities
2520            .keys()
2521            .map(|id| (id, inner.reverse_adjacency.get(id).map_or(0, |v| v.len())))
2522            .collect();
2523        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2524        pairs.truncate(n);
2525        Ok(pairs
2526            .into_iter()
2527            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2528            .collect())
2529    }
2530}
2531
2532impl Default for GraphStore {
2533    fn default() -> Self {
2534        Self::new()
2535    }
2536}
2537
2538// ── Tests ─────────────────────────────────────────────────────────────────────
2539
2540#[cfg(test)]
2541mod tests {
2542    use super::*;
2543
2544    fn make_graph() -> GraphStore {
2545        GraphStore::new()
2546    }
2547
2548    fn add(g: &GraphStore, id: &str) {
2549        g.add_entity(Entity::new(id, "Node")).unwrap();
2550    }
2551
2552    fn link(g: &GraphStore, from: &str, to: &str) {
2553        g.add_relationship(Relationship::new(from, to, "CONNECTS", 1.0))
2554            .unwrap();
2555    }
2556
2557    fn link_w(g: &GraphStore, from: &str, to: &str, weight: f32) {
2558        g.add_relationship(Relationship::new(from, to, "CONNECTS", weight))
2559            .unwrap();
2560    }
2561
2562    // ── EntityId ──────────────────────────────────────────────────────────────
2563
2564    #[test]
2565    fn test_entity_id_equality() {
2566        assert_eq!(EntityId::new("a"), EntityId::new("a"));
2567        assert_ne!(EntityId::new("a"), EntityId::new("b"));
2568    }
2569
2570    #[test]
2571    fn test_entity_id_display() {
2572        let id = EntityId::new("hello");
2573        assert_eq!(id.to_string(), "hello");
2574    }
2575
2576    // ── Entity ────────────────────────────────────────────────────────────────
2577
2578    #[test]
2579    fn test_entity_new_has_empty_properties() {
2580        let e = Entity::new("e1", "Person");
2581        assert!(e.properties.is_empty());
2582    }
2583
2584    #[test]
2585    fn test_entity_with_properties_stores_props() {
2586        let mut props = HashMap::new();
2587        props.insert("age".into(), Value::Number(42.into()));
2588        let e = Entity::with_properties("e1", "Person", props);
2589        assert!(e.properties.contains_key("age"));
2590    }
2591
2592    // ── GraphStore basic ops ──────────────────────────────────────────────────
2593
2594    #[test]
2595    fn test_graph_add_entity_increments_count() {
2596        let g = make_graph();
2597        add(&g, "a");
2598        assert_eq!(g.entity_count().unwrap(), 1);
2599    }
2600
2601    #[test]
2602    fn test_graph_get_entity_returns_entity() {
2603        let g = make_graph();
2604        g.add_entity(Entity::new("e1", "Person")).unwrap();
2605        let e = g.get_entity(&EntityId::new("e1")).unwrap();
2606        assert_eq!(e.label, "Person");
2607    }
2608
2609    #[test]
2610    fn test_graph_get_entity_missing_returns_error() {
2611        let g = make_graph();
2612        assert!(g.get_entity(&EntityId::new("ghost")).is_err());
2613    }
2614
2615    #[test]
2616    fn test_graph_add_relationship_increments_count() {
2617        let g = make_graph();
2618        add(&g, "a");
2619        add(&g, "b");
2620        link(&g, "a", "b");
2621        assert_eq!(g.relationship_count().unwrap(), 1);
2622    }
2623
2624    #[test]
2625    fn test_graph_add_relationship_missing_source_fails() {
2626        let g = make_graph();
2627        add(&g, "b");
2628        let result = g.add_relationship(Relationship::new("ghost", "b", "X", 1.0));
2629        assert!(result.is_err());
2630    }
2631
2632    #[test]
2633    fn test_graph_add_relationship_missing_target_fails() {
2634        let g = make_graph();
2635        add(&g, "a");
2636        let result = g.add_relationship(Relationship::new("a", "ghost", "X", 1.0));
2637        assert!(result.is_err());
2638    }
2639
2640    #[test]
2641    fn test_graph_remove_entity_removes_relationships() {
2642        let g = make_graph();
2643        add(&g, "a");
2644        add(&g, "b");
2645        link(&g, "a", "b");
2646        g.remove_entity(&EntityId::new("a")).unwrap();
2647        assert_eq!(g.entity_count().unwrap(), 1);
2648        assert_eq!(g.relationship_count().unwrap(), 0);
2649    }
2650
2651    #[test]
2652    fn test_graph_remove_entity_missing_returns_error() {
2653        let g = make_graph();
2654        assert!(g.remove_entity(&EntityId::new("ghost")).is_err());
2655    }
2656
2657    // ── BFS ───────────────────────────────────────────────────────────────────
2658
2659    #[test]
2660    fn test_bfs_finds_direct_neighbours() {
2661        let g = make_graph();
2662        add(&g, "a");
2663        add(&g, "b");
2664        add(&g, "c");
2665        link(&g, "a", "b");
2666        link(&g, "a", "c");
2667        let visited = g.bfs(&EntityId::new("a")).unwrap();
2668        assert_eq!(visited.len(), 2);
2669    }
2670
2671    #[test]
2672    fn test_bfs_traverses_chain() {
2673        let g = make_graph();
2674        add(&g, "a");
2675        add(&g, "b");
2676        add(&g, "c");
2677        add(&g, "d");
2678        link(&g, "a", "b");
2679        link(&g, "b", "c");
2680        link(&g, "c", "d");
2681        let visited = g.bfs(&EntityId::new("a")).unwrap();
2682        assert_eq!(visited.len(), 3);
2683        assert_eq!(visited[0], EntityId::new("b"));
2684    }
2685
2686    #[test]
2687    fn test_bfs_handles_isolated_node() {
2688        let g = make_graph();
2689        add(&g, "a");
2690        let visited = g.bfs(&EntityId::new("a")).unwrap();
2691        assert!(visited.is_empty());
2692    }
2693
2694    #[test]
2695    fn test_bfs_missing_start_returns_error() {
2696        let g = make_graph();
2697        assert!(g.bfs(&EntityId::new("ghost")).is_err());
2698    }
2699
2700    // ── DFS ───────────────────────────────────────────────────────────────────
2701
2702    #[test]
2703    fn test_dfs_visits_all_reachable_nodes() {
2704        let g = make_graph();
2705        add(&g, "a");
2706        add(&g, "b");
2707        add(&g, "c");
2708        add(&g, "d");
2709        link(&g, "a", "b");
2710        link(&g, "a", "c");
2711        link(&g, "b", "d");
2712        let visited = g.dfs(&EntityId::new("a")).unwrap();
2713        assert_eq!(visited.len(), 3);
2714    }
2715
2716    #[test]
2717    fn test_dfs_handles_isolated_node() {
2718        let g = make_graph();
2719        add(&g, "a");
2720        let visited = g.dfs(&EntityId::new("a")).unwrap();
2721        assert!(visited.is_empty());
2722    }
2723
2724    #[test]
2725    fn test_dfs_missing_start_returns_error() {
2726        let g = make_graph();
2727        assert!(g.dfs(&EntityId::new("ghost")).is_err());
2728    }
2729
2730    // ── Shortest path ─────────────────────────────────────────────────────────
2731
2732    #[test]
2733    fn test_shortest_path_direct_connection() {
2734        let g = make_graph();
2735        add(&g, "a");
2736        add(&g, "b");
2737        link(&g, "a", "b");
2738        let path = g
2739            .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
2740            .unwrap();
2741        assert_eq!(path, Some(vec![EntityId::new("a"), EntityId::new("b")]));
2742    }
2743
2744    #[test]
2745    fn test_shortest_path_multi_hop() {
2746        let g = make_graph();
2747        add(&g, "a");
2748        add(&g, "b");
2749        add(&g, "c");
2750        link(&g, "a", "b");
2751        link(&g, "b", "c");
2752        let path = g
2753            .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
2754            .unwrap();
2755        assert_eq!(path.as_ref().map(|p| p.len()), Some(3));
2756    }
2757
2758    #[test]
2759    fn test_shortest_path_returns_none_for_disconnected() {
2760        let g = make_graph();
2761        add(&g, "a");
2762        add(&g, "b");
2763        let path = g
2764            .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
2765            .unwrap();
2766        assert_eq!(path, None);
2767    }
2768
2769    #[test]
2770    fn test_shortest_path_same_node_returns_single_element() {
2771        let g = make_graph();
2772        add(&g, "a");
2773        let path = g
2774            .shortest_path(&EntityId::new("a"), &EntityId::new("a"))
2775            .unwrap();
2776        assert_eq!(path, Some(vec![EntityId::new("a")]));
2777    }
2778
2779    #[test]
2780    fn test_shortest_path_missing_source_returns_error() {
2781        let g = make_graph();
2782        add(&g, "b");
2783        assert!(g
2784            .shortest_path(&EntityId::new("ghost"), &EntityId::new("b"))
2785            .is_err());
2786    }
2787
2788    #[test]
2789    fn test_shortest_path_missing_target_returns_error() {
2790        let g = make_graph();
2791        add(&g, "a");
2792        assert!(g
2793            .shortest_path(&EntityId::new("a"), &EntityId::new("ghost"))
2794            .is_err());
2795    }
2796
2797    // ── Transitive closure ────────────────────────────────────────────────────
2798
2799    #[test]
2800    fn test_transitive_closure_includes_start() {
2801        let g = make_graph();
2802        add(&g, "a");
2803        add(&g, "b");
2804        link(&g, "a", "b");
2805        let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
2806        assert!(closure.contains(&EntityId::new("a")));
2807        assert!(closure.contains(&EntityId::new("b")));
2808    }
2809
2810    #[test]
2811    fn test_transitive_closure_isolated_node_contains_only_self() {
2812        let g = make_graph();
2813        add(&g, "a");
2814        let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
2815        assert_eq!(closure.len(), 1);
2816    }
2817
2818    // ── MemGraphError conversion ──────────────────────────────────────────────
2819
2820    #[test]
2821    fn test_mem_graph_error_converts_to_runtime_error() {
2822        let e = MemGraphError::EntityNotFound("x".into());
2823        let re: AgentRuntimeError = e.into();
2824        assert!(matches!(re, AgentRuntimeError::Graph(_)));
2825    }
2826
2827    // ── Weighted shortest path ────────────────────────────────────────────────
2828
2829    #[test]
2830    fn test_shortest_path_weighted_simple() {
2831        // a --(1.0)--> b --(2.0)--> c
2832        // a --(10.0)--> c  (direct but heavier)
2833        let g = make_graph();
2834        add(&g, "a");
2835        add(&g, "b");
2836        add(&g, "c");
2837        link_w(&g, "a", "b", 1.0);
2838        link_w(&g, "b", "c", 2.0);
2839        g.add_relationship(Relationship::new("a", "c", "DIRECT", 10.0))
2840            .unwrap();
2841
2842        let result = g
2843            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("c"))
2844            .unwrap();
2845        assert!(result.is_some());
2846        let (path, weight) = result.unwrap();
2847        // The cheapest path is a -> b -> c with total weight 3.0
2848        assert_eq!(
2849            path,
2850            vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")]
2851        );
2852        assert!((weight - 3.0).abs() < 1e-5);
2853    }
2854
2855    #[test]
2856    fn test_shortest_path_weighted_returns_none_for_disconnected() {
2857        let g = make_graph();
2858        add(&g, "a");
2859        add(&g, "b");
2860        let result = g
2861            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"))
2862            .unwrap();
2863        assert!(result.is_none());
2864    }
2865
2866    #[test]
2867    fn test_shortest_path_weighted_same_node() {
2868        let g = make_graph();
2869        add(&g, "a");
2870        let result = g
2871            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("a"))
2872            .unwrap();
2873        assert_eq!(result, Some((vec![EntityId::new("a")], 0.0)));
2874    }
2875
2876    #[test]
2877    fn test_shortest_path_weighted_negative_weight_errors() {
2878        let g = make_graph();
2879        add(&g, "a");
2880        add(&g, "b");
2881        g.add_relationship(Relationship::new("a", "b", "NEG", -1.0))
2882            .unwrap();
2883        let result = g.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"));
2884        assert!(result.is_err());
2885    }
2886
2887    // ── Degree centrality ─────────────────────────────────────────────────────
2888
2889    #[test]
2890    fn test_degree_centrality_basic() {
2891        // Star graph: a -> b, a -> c, a -> d
2892        // a: out=3, in=0 => (3+0)/(4-1) = 1.0
2893        // b: out=0, in=1 => (0+1)/3 = 0.333...
2894        let g = make_graph();
2895        add(&g, "a");
2896        add(&g, "b");
2897        add(&g, "c");
2898        add(&g, "d");
2899        link(&g, "a", "b");
2900        link(&g, "a", "c");
2901        link(&g, "a", "d");
2902
2903        let centrality = g.degree_centrality().unwrap();
2904        let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
2905        let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
2906
2907        assert!((a_cent - 1.0).abs() < 1e-5, "a centrality was {a_cent}");
2908        assert!(
2909            (b_cent - 1.0 / 3.0).abs() < 1e-5,
2910            "b centrality was {b_cent}"
2911        );
2912    }
2913
2914    // ── Betweenness centrality ────────────────────────────────────────────────
2915
2916    #[test]
2917    fn test_betweenness_centrality_chain() {
2918        // Chain: a -> b -> c -> d
2919        // b and c are on all paths from a to c, a to d, b to d
2920        // Node b and c should have higher centrality than a and d.
2921        let g = make_graph();
2922        add(&g, "a");
2923        add(&g, "b");
2924        add(&g, "c");
2925        add(&g, "d");
2926        link(&g, "a", "b");
2927        link(&g, "b", "c");
2928        link(&g, "c", "d");
2929
2930        let centrality = g.betweenness_centrality().unwrap();
2931        let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
2932        let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
2933        let c_cent = *centrality.get(&EntityId::new("c")).unwrap();
2934        let d_cent = *centrality.get(&EntityId::new("d")).unwrap();
2935
2936        // Endpoints should have 0 centrality; intermediate nodes should be > 0.
2937        assert!((a_cent).abs() < 1e-5, "expected a_cent ~ 0, got {a_cent}");
2938        assert!(b_cent > 0.0, "expected b_cent > 0, got {b_cent}");
2939        assert!(c_cent > 0.0, "expected c_cent > 0, got {c_cent}");
2940        assert!((d_cent).abs() < 1e-5, "expected d_cent ~ 0, got {d_cent}");
2941    }
2942
2943    // ── Label propagation communities ─────────────────────────────────────────
2944
2945    #[test]
2946    fn test_label_propagation_communities_two_clusters() {
2947        // Cluster 1: a <-> b <-> c (fully connected via bidirectional edges)
2948        // Cluster 2: x <-> y <-> z
2949        // No edges between clusters.
2950        let g = make_graph();
2951        for id in &["a", "b", "c", "x", "y", "z"] {
2952            add(&g, id);
2953        }
2954        // Cluster 1 (bidirectional via two directed edges each)
2955        link(&g, "a", "b");
2956        link(&g, "b", "a");
2957        link(&g, "b", "c");
2958        link(&g, "c", "b");
2959        link(&g, "a", "c");
2960        link(&g, "c", "a");
2961        // Cluster 2
2962        link(&g, "x", "y");
2963        link(&g, "y", "x");
2964        link(&g, "y", "z");
2965        link(&g, "z", "y");
2966        link(&g, "x", "z");
2967        link(&g, "z", "x");
2968
2969        let communities = g.label_propagation_communities(100).unwrap();
2970
2971        let label_a = communities[&EntityId::new("a")];
2972        let label_b = communities[&EntityId::new("b")];
2973        let label_c = communities[&EntityId::new("c")];
2974        let label_x = communities[&EntityId::new("x")];
2975        let label_y = communities[&EntityId::new("y")];
2976        let label_z = communities[&EntityId::new("z")];
2977
2978        // All nodes in cluster 1 share a label, all in cluster 2 share a label,
2979        // and the two clusters have different labels.
2980        assert_eq!(label_a, label_b, "a and b should be in same community");
2981        assert_eq!(label_b, label_c, "b and c should be in same community");
2982        assert_eq!(label_x, label_y, "x and y should be in same community");
2983        assert_eq!(label_y, label_z, "y and z should be in same community");
2984        assert_ne!(
2985            label_a, label_x,
2986            "cluster 1 and cluster 2 should be different communities"
2987        );
2988    }
2989
2990    // ── Subgraph extraction ───────────────────────────────────────────────────
2991
2992    #[test]
2993    fn test_subgraph_extracts_correct_nodes_and_edges() {
2994        // Full graph: a -> b -> c -> d
2995        // Subgraph of {a, b, c} should contain edges a->b and b->c but not c->d.
2996        let g = make_graph();
2997        add(&g, "a");
2998        add(&g, "b");
2999        add(&g, "c");
3000        add(&g, "d");
3001        link(&g, "a", "b");
3002        link(&g, "b", "c");
3003        link(&g, "c", "d");
3004
3005        let sub = g
3006            .subgraph(&[EntityId::new("a"), EntityId::new("b"), EntityId::new("c")])
3007            .unwrap();
3008
3009        assert_eq!(sub.entity_count().unwrap(), 3);
3010        assert_eq!(sub.relationship_count().unwrap(), 2);
3011
3012        // d should not be present in the subgraph.
3013        assert!(sub.get_entity(&EntityId::new("d")).is_err());
3014
3015        // a -> b and b -> c should be present; c -> d should not.
3016        let path = sub
3017            .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
3018            .unwrap();
3019        assert!(path.is_some());
3020        assert_eq!(path.unwrap().len(), 3);
3021    }
3022
3023    // ── detect_cycles ──────────────────────────────────────────────────────────
3024
3025    #[test]
3026    fn test_detect_cycles_dag_returns_false() {
3027        let g = make_graph();
3028        add(&g, "a");
3029        add(&g, "b");
3030        add(&g, "c");
3031        link(&g, "a", "b");
3032        link(&g, "b", "c");
3033        assert_eq!(g.detect_cycles().unwrap(), false);
3034    }
3035
3036    #[test]
3037    fn test_detect_cycles_self_loop_returns_true() {
3038        let g = make_graph();
3039        add(&g, "a");
3040        // Use a different kind to avoid duplicate-relationship rejection.
3041        g.add_relationship(Relationship::new("a", "a", "SELF", 1.0))
3042            .unwrap();
3043        assert_eq!(g.detect_cycles().unwrap(), true);
3044    }
3045
3046    #[test]
3047    fn test_detect_cycles_simple_cycle_returns_true() {
3048        let g = make_graph();
3049        add(&g, "a");
3050        add(&g, "b");
3051        link(&g, "a", "b");
3052        g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
3053            .unwrap();
3054        assert_eq!(g.detect_cycles().unwrap(), true);
3055    }
3056
3057    #[test]
3058    fn test_detect_cycles_empty_graph_returns_false() {
3059        let g = make_graph();
3060        assert_eq!(g.detect_cycles().unwrap(), false);
3061    }
3062
3063    #[test]
3064    fn test_detect_cycles_result_is_cached() {
3065        let g = make_graph();
3066        add(&g, "x");
3067        add(&g, "y");
3068        link(&g, "x", "y");
3069        // First call.
3070        let r1 = g.detect_cycles().unwrap();
3071        // Second call should return the cached value.
3072        let r2 = g.detect_cycles().unwrap();
3073        assert_eq!(r1, r2);
3074    }
3075
3076    #[test]
3077    fn test_detect_cycles_cache_invalidated_on_mutation() {
3078        let g = make_graph();
3079        add(&g, "a");
3080        add(&g, "b");
3081        link(&g, "a", "b");
3082        assert_eq!(g.detect_cycles().unwrap(), false);
3083
3084        // Add a back edge to create a cycle — cache must be invalidated.
3085        g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
3086            .unwrap();
3087        assert_eq!(
3088            g.detect_cycles().unwrap(),
3089            true,
3090            "cache should be invalidated after adding a back edge"
3091        );
3092    }
3093
3094    // ── bfs_bounded / dfs_bounded ─────────────────────────────────────────────
3095
3096    #[test]
3097    fn test_bfs_bounded_respects_max_depth() {
3098        // Chain: a -> b -> c -> d
3099        let g = make_graph();
3100        add(&g, "a");
3101        add(&g, "b");
3102        add(&g, "c");
3103        add(&g, "d");
3104        link(&g, "a", "b");
3105        link(&g, "b", "c");
3106        link(&g, "c", "d");
3107
3108        // max_depth=1 should only visit a and b (depth 0 and 1)
3109        let visited = g.bfs_bounded("a", 1, 100).unwrap();
3110        assert!(visited.contains(&EntityId::new("a")));
3111        assert!(visited.contains(&EntityId::new("b")));
3112        assert!(!visited.contains(&EntityId::new("c")), "c is at depth 2, should not be visited");
3113    }
3114
3115    // ── #5/#35 path_exists ────────────────────────────────────────────────────
3116
3117    #[test]
3118    fn test_path_exists_returns_true() {
3119        let g = make_graph();
3120        add(&g, "a");
3121        add(&g, "b");
3122        add(&g, "c");
3123        link(&g, "a", "b");
3124        link(&g, "b", "c");
3125        assert_eq!(g.path_exists("a", "c").unwrap(), true);
3126    }
3127
3128    #[test]
3129    fn test_path_exists_returns_false() {
3130        let g = make_graph();
3131        add(&g, "a");
3132        add(&g, "b");
3133        assert_eq!(g.path_exists("a", "b").unwrap(), false);
3134    }
3135
3136    // ── #13 EntityId::as_str ──────────────────────────────────────────────────
3137
3138    #[test]
3139    fn test_entity_id_as_str() {
3140        let id = EntityId::new("my-entity");
3141        assert_eq!(id.as_str(), "my-entity");
3142    }
3143
3144    // ── #38 EntityId AsRef<str> ───────────────────────────────────────────────
3145
3146    #[test]
3147    fn test_entity_id_as_ref_str() {
3148        let id = EntityId::new("asref-test");
3149        let s: &str = id.as_ref();
3150        assert_eq!(s, "asref-test");
3151    }
3152
3153    #[test]
3154    fn test_dfs_bounded_respects_max_nodes() {
3155        // Chain: a -> b -> c -> d
3156        let g = make_graph();
3157        add(&g, "a");
3158        add(&g, "b");
3159        add(&g, "c");
3160        add(&g, "d");
3161        link(&g, "a", "b");
3162        link(&g, "b", "c");
3163        link(&g, "c", "d");
3164
3165        // max_nodes=2 means only 2 nodes total
3166        let visited = g.dfs_bounded("a", 100, 2).unwrap();
3167        assert_eq!(visited.len(), 2, "should stop at 2 nodes");
3168    }
3169
3170    // ── New API tests (Rounds 4-8) ────────────────────────────────────────────
3171
3172    #[test]
3173    fn test_entity_exists_and_relationship_exists() {
3174        let g = GraphStore::new();
3175        let a = EntityId::new("a");
3176        let b = EntityId::new("b");
3177        assert!(!g.entity_exists(&a).unwrap());
3178        g.add_entity(Entity::new("a", "Node")).unwrap();
3179        assert!(g.entity_exists(&a).unwrap());
3180        assert!(!g.relationship_exists(&a, &b, "knows").unwrap());
3181        g.add_entity(Entity::new("b", "Node")).unwrap();
3182        g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
3183        assert!(g.relationship_exists(&a, &b, "knows").unwrap());
3184        assert!(!g.relationship_exists(&a, &b, "likes").unwrap());
3185    }
3186
3187    #[test]
3188    fn test_get_relationships_for_returns_outgoing() {
3189        let g = GraphStore::new();
3190        g.add_entity(Entity::new("a", "N")).unwrap();
3191        g.add_entity(Entity::new("b", "N")).unwrap();
3192        g.add_entity(Entity::new("c", "N")).unwrap();
3193        let a = EntityId::new("a");
3194        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3195        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
3196        let rels = g.get_relationships_for(&a).unwrap();
3197        assert_eq!(rels.len(), 2);
3198    }
3199
3200    #[test]
3201    fn test_relationships_between_finds_both_directions() {
3202        let g = GraphStore::new();
3203        g.add_entity(Entity::new("x", "N")).unwrap();
3204        g.add_entity(Entity::new("y", "N")).unwrap();
3205        let x = EntityId::new("x");
3206        let y = EntityId::new("y");
3207        g.add_relationship(Relationship::new("x", "y", "follows", 1.0)).unwrap();
3208        g.add_relationship(Relationship::new("y", "x", "blocks", 1.0)).unwrap();
3209        let rels = g.relationships_between(&x, &y).unwrap();
3210        assert_eq!(rels.len(), 2);
3211    }
3212
3213    #[test]
3214    fn test_find_entities_by_property() {
3215        let g = GraphStore::new();
3216        g.add_entity(Entity::new("a", "Person").with_property("age", serde_json::json!(30))).unwrap();
3217        g.add_entity(Entity::new("b", "Person").with_property("age", serde_json::json!(25))).unwrap();
3218        g.add_entity(Entity::new("c", "Person").with_property("age", serde_json::json!(30))).unwrap();
3219        let found = g.find_entities_by_property("age", &serde_json::json!(30)).unwrap();
3220        assert_eq!(found.len(), 2);
3221        let ids: Vec<_> = found.iter().map(|e| e.id.as_str()).collect();
3222        assert!(ids.contains(&"a") && ids.contains(&"c"));
3223    }
3224
3225    #[test]
3226    fn test_neighbor_entities_returns_entity_objects() {
3227        let g = GraphStore::new();
3228        g.add_entity(Entity::new("root", "R")).unwrap();
3229        g.add_entity(Entity::new("child1", "C")).unwrap();
3230        g.add_entity(Entity::new("child2", "C")).unwrap();
3231        let root = EntityId::new("root");
3232        g.add_relationship(Relationship::new("root", "child1", "has", 1.0)).unwrap();
3233        g.add_relationship(Relationship::new("root", "child2", "has", 1.0)).unwrap();
3234        let neighbors = g.neighbor_entities(&root).unwrap();
3235        assert_eq!(neighbors.len(), 2);
3236        let labels: Vec<_> = neighbors.iter().map(|e| e.label.as_str()).collect();
3237        assert!(labels.iter().all(|l| *l == "C"));
3238    }
3239
3240    #[test]
3241    fn test_remove_all_relationships_for() {
3242        let g = GraphStore::new();
3243        g.add_entity(Entity::new("a", "N")).unwrap();
3244        g.add_entity(Entity::new("b", "N")).unwrap();
3245        let a = EntityId::new("a");
3246        g.add_relationship(Relationship::new("a", "b", "r1", 1.0)).unwrap();
3247        g.add_relationship(Relationship::new("a", "b", "r2", 1.0)).unwrap();
3248        let removed = g.remove_all_relationships_for(&a).unwrap();
3249        assert_eq!(removed, 2);
3250        assert_eq!(g.relationship_count().unwrap(), 0);
3251    }
3252
3253    #[test]
3254    fn test_topological_sort_returns_valid_order() {
3255        let g = GraphStore::new();
3256        for id in &["a", "b", "c", "d"] {
3257            g.add_entity(Entity::new(*id, "N")).unwrap();
3258        }
3259        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3260        g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
3261        g.add_relationship(Relationship::new("c", "d", "r", 1.0)).unwrap();
3262        let order = g.topological_sort().unwrap();
3263        assert_eq!(order.len(), 4);
3264        let pos: std::collections::HashMap<_, _> = order.iter().enumerate().map(|(i, id)| (id.as_str().to_owned(), i)).collect();
3265        assert!(pos["a"] < pos["b"]);
3266        assert!(pos["b"] < pos["c"]);
3267        assert!(pos["c"] < pos["d"]);
3268    }
3269
3270    #[test]
3271    fn test_topological_sort_rejects_cycle() {
3272        let g = GraphStore::new();
3273        g.add_entity(Entity::new("x", "N")).unwrap();
3274        g.add_entity(Entity::new("y", "N")).unwrap();
3275        g.add_relationship(Relationship::new("x", "y", "r", 1.0)).unwrap();
3276        g.add_relationship(Relationship::new("y", "x", "r", 1.0)).unwrap();
3277        assert!(g.topological_sort().is_err());
3278    }
3279
3280    #[test]
3281    fn test_entity_count_by_label() {
3282        let g = GraphStore::new();
3283        g.add_entity(Entity::new("a", "Person")).unwrap();
3284        g.add_entity(Entity::new("b", "Person")).unwrap();
3285        g.add_entity(Entity::new("c", "Organization")).unwrap();
3286        assert_eq!(g.entity_count_by_label("Person").unwrap(), 2);
3287        assert_eq!(g.entity_count_by_label("Organization").unwrap(), 1);
3288        assert_eq!(g.entity_count_by_label("Unknown").unwrap(), 0);
3289    }
3290
3291    #[test]
3292    fn test_update_entity_label_and_property() {
3293        let g = GraphStore::new();
3294        let id = EntityId::new("e1");
3295        g.add_entity(Entity::new("e1", "Old")).unwrap();
3296        assert!(g.update_entity_label(&id, "New").unwrap());
3297        assert_eq!(g.get_entity(&id).unwrap().label, "New");
3298        assert!(g.update_entity_property(&id, "key", serde_json::json!("val")).unwrap());
3299        assert_eq!(g.get_entity(&id).unwrap().properties["key"], serde_json::json!("val"));
3300    }
3301
3302    #[test]
3303    fn test_connected_components_single_node() {
3304        let g = GraphStore::new();
3305        g.add_entity(Entity::new("a", "A")).unwrap();
3306        assert_eq!(g.connected_components().unwrap(), 1);
3307    }
3308
3309    #[test]
3310    fn test_connected_components_two_isolated_nodes() {
3311        let g = GraphStore::new();
3312        g.add_entity(Entity::new("a", "A")).unwrap();
3313        g.add_entity(Entity::new("b", "B")).unwrap();
3314        assert_eq!(g.connected_components().unwrap(), 2);
3315    }
3316
3317    #[test]
3318    fn test_connected_components_connected_pair() {
3319        let g = GraphStore::new();
3320        g.add_entity(Entity::new("a", "A")).unwrap();
3321        g.add_entity(Entity::new("b", "B")).unwrap();
3322        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3323        assert_eq!(g.connected_components().unwrap(), 1);
3324    }
3325
3326    #[test]
3327    fn test_connected_components_two_separate_pairs() {
3328        let g = GraphStore::new();
3329        g.add_entity(Entity::new("a", "A")).unwrap();
3330        g.add_entity(Entity::new("b", "B")).unwrap();
3331        g.add_entity(Entity::new("c", "C")).unwrap();
3332        g.add_entity(Entity::new("d", "D")).unwrap();
3333        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3334        g.add_relationship(Relationship::new("c", "d", "link", 1.0)).unwrap();
3335        assert_eq!(g.connected_components().unwrap(), 2);
3336    }
3337
3338    #[test]
3339    fn test_connected_components_empty_graph() {
3340        let g = GraphStore::new();
3341        assert_eq!(g.connected_components().unwrap(), 0);
3342    }
3343
3344    // ── Round 9: weakly_connected ─────────────────────────────────────────────
3345
3346    #[test]
3347    fn test_weakly_connected_true_for_empty_graph() {
3348        let g = GraphStore::new();
3349        assert!(g.weakly_connected().unwrap());
3350    }
3351
3352    #[test]
3353    fn test_weakly_connected_true_for_single_node() {
3354        let g = GraphStore::new();
3355        g.add_entity(Entity::new("a", "A")).unwrap();
3356        assert!(g.weakly_connected().unwrap());
3357    }
3358
3359    #[test]
3360    fn test_weakly_connected_true_when_all_nodes_connected() {
3361        let g = GraphStore::new();
3362        g.add_entity(Entity::new("a", "A")).unwrap();
3363        g.add_entity(Entity::new("b", "B")).unwrap();
3364        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3365        assert!(g.weakly_connected().unwrap());
3366    }
3367
3368    #[test]
3369    fn test_weakly_connected_false_when_nodes_isolated() {
3370        let g = GraphStore::new();
3371        g.add_entity(Entity::new("a", "A")).unwrap();
3372        g.add_entity(Entity::new("b", "B")).unwrap();
3373        assert!(!g.weakly_connected().unwrap());
3374    }
3375
3376    #[test]
3377    fn test_isolates_returns_nodes_with_no_edges() {
3378        let g = GraphStore::new();
3379        g.add_entity(Entity::new("a", "A")).unwrap();
3380        g.add_entity(Entity::new("b", "B")).unwrap();
3381        g.add_entity(Entity::new("c", "C")).unwrap();
3382        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3383        let iso = g.isolates().unwrap();
3384        let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
3385        ids.sort();
3386        assert_eq!(ids, vec!["c".to_string()]);
3387    }
3388
3389    #[test]
3390    fn test_isolates_returns_empty_when_all_connected() {
3391        let g = GraphStore::new();
3392        g.add_entity(Entity::new("a", "A")).unwrap();
3393        g.add_entity(Entity::new("b", "B")).unwrap();
3394        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3395        assert!(g.isolates().unwrap().is_empty());
3396    }
3397
3398    #[test]
3399    fn test_isolates_all_isolated() {
3400        let g = GraphStore::new();
3401        g.add_entity(Entity::new("x", "X")).unwrap();
3402        g.add_entity(Entity::new("y", "Y")).unwrap();
3403        let iso = g.isolates().unwrap();
3404        let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
3405        ids.sort();
3406        assert_eq!(ids, vec!["x".to_string(), "y".to_string()]);
3407    }
3408
3409    #[test]
3410    fn test_is_dag_on_dag() {
3411        let g = GraphStore::new();
3412        g.add_entity(Entity::new("a", "A")).unwrap();
3413        g.add_entity(Entity::new("b", "B")).unwrap();
3414        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3415        assert!(g.is_dag().unwrap());
3416    }
3417
3418    #[test]
3419    fn test_is_dag_on_cyclic_graph() {
3420        let g = GraphStore::new();
3421        g.add_entity(Entity::new("a", "A")).unwrap();
3422        g.add_entity(Entity::new("b", "B")).unwrap();
3423        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3424        g.add_relationship(Relationship::new("b", "a", "back", 1.0)).unwrap();
3425        assert!(!g.is_dag().unwrap());
3426    }
3427
3428    #[test]
3429    fn test_in_degree_and_out_degree() {
3430        let g = GraphStore::new();
3431        g.add_entity(Entity::new("a", "A")).unwrap();
3432        g.add_entity(Entity::new("b", "B")).unwrap();
3433        g.add_entity(Entity::new("c", "C")).unwrap();
3434        g.add_relationship(Relationship::new("a", "b", "e1", 1.0)).unwrap();
3435        g.add_relationship(Relationship::new("c", "b", "e2", 1.0)).unwrap();
3436        let a = EntityId::new("a");
3437        let b = EntityId::new("b");
3438        let c = EntityId::new("c");
3439        assert_eq!(g.out_degree(&a).unwrap(), 1);
3440        assert_eq!(g.in_degree(&a).unwrap(), 0);
3441        assert_eq!(g.in_degree(&b).unwrap(), 2);
3442        assert_eq!(g.out_degree(&b).unwrap(), 0);
3443        assert_eq!(g.out_degree(&c).unwrap(), 1);
3444        assert_eq!(g.in_degree(&c).unwrap(), 0);
3445    }
3446
3447    #[test]
3448    fn test_in_degree_missing_entity_returns_zero() {
3449        let g = GraphStore::new();
3450        let id = EntityId::new("ghost");
3451        assert_eq!(g.in_degree(&id).unwrap(), 0);
3452    }
3453
3454    #[test]
3455    fn test_out_degree_missing_entity_returns_zero() {
3456        let g = GraphStore::new();
3457        let id = EntityId::new("ghost");
3458        assert_eq!(g.out_degree(&id).unwrap(), 0);
3459    }
3460
3461    #[test]
3462    fn test_node_count_is_alias_for_entity_count() {
3463        let g = GraphStore::new();
3464        g.add_entity(Entity::new("a", "A")).unwrap();
3465        g.add_entity(Entity::new("b", "B")).unwrap();
3466        assert_eq!(g.node_count().unwrap(), g.entity_count().unwrap());
3467        assert_eq!(g.node_count().unwrap(), 2);
3468    }
3469
3470    #[test]
3471    fn test_edge_count_is_alias_for_relationship_count() {
3472        let g = GraphStore::new();
3473        g.add_entity(Entity::new("a", "A")).unwrap();
3474        g.add_entity(Entity::new("b", "B")).unwrap();
3475        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3476        assert_eq!(g.edge_count().unwrap(), g.relationship_count().unwrap());
3477        assert_eq!(g.edge_count().unwrap(), 1);
3478    }
3479
3480    #[test]
3481    fn test_source_nodes_returns_nodes_with_no_incoming_edges() {
3482        let g = GraphStore::new();
3483        g.add_entity(Entity::new("root", "Root")).unwrap();
3484        g.add_entity(Entity::new("child", "Child")).unwrap();
3485        g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
3486        g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
3487        g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
3488        let sources = g.source_nodes().unwrap();
3489        assert_eq!(sources.len(), 1);
3490        assert_eq!(sources[0].id.as_str(), "root");
3491    }
3492
3493    #[test]
3494    fn test_sink_nodes_returns_nodes_with_no_outgoing_edges() {
3495        let g = GraphStore::new();
3496        g.add_entity(Entity::new("root", "Root")).unwrap();
3497        g.add_entity(Entity::new("child", "Child")).unwrap();
3498        g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
3499        g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
3500        g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
3501        let sinks = g.sink_nodes().unwrap();
3502        assert_eq!(sinks.len(), 1);
3503        assert_eq!(sinks[0].id.as_str(), "leaf");
3504    }
3505
3506    #[test]
3507    fn test_source_and_sink_empty_on_isolated_node() {
3508        // An isolated node has no in or out edges, so it's both source and sink
3509        let g = GraphStore::new();
3510        g.add_entity(Entity::new("solo", "Solo")).unwrap();
3511        assert_eq!(g.source_nodes().unwrap().len(), 1);
3512        assert_eq!(g.sink_nodes().unwrap().len(), 1);
3513    }
3514
3515    #[test]
3516    fn test_reverse_flips_all_edges() {
3517        let g = GraphStore::new();
3518        g.add_entity(Entity::new("a", "A")).unwrap();
3519        g.add_entity(Entity::new("b", "B")).unwrap();
3520        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3521        let rev = g.reverse().unwrap();
3522        // Original: a→b. Reversed: b→a.
3523        assert_eq!(rev.entity_count().unwrap(), 2);
3524        assert_eq!(rev.relationship_count().unwrap(), 1);
3525        let b_id = EntityId::new("b");
3526        let a_id = EntityId::new("a");
3527        assert!(rev.relationship_exists(&b_id, &a_id, "edge").unwrap());
3528    }
3529
3530    #[test]
3531    fn test_reverse_empty_graph_stays_empty() {
3532        let g = GraphStore::new();
3533        let rev = g.reverse().unwrap();
3534        assert_eq!(rev.entity_count().unwrap(), 0);
3535    }
3536
3537    #[test]
3538    fn test_common_neighbors_finds_shared_targets() {
3539        let g = GraphStore::new();
3540        g.add_entity(Entity::new("a", "A")).unwrap();
3541        g.add_entity(Entity::new("b", "B")).unwrap();
3542        g.add_entity(Entity::new("shared", "S")).unwrap();
3543        g.add_entity(Entity::new("only_a", "OA")).unwrap();
3544        g.add_relationship(Relationship::new("a", "shared", "e", 1.0)).unwrap();
3545        g.add_relationship(Relationship::new("b", "shared", "e", 1.0)).unwrap();
3546        g.add_relationship(Relationship::new("a", "only_a", "e", 1.0)).unwrap();
3547        let a_id = EntityId::new("a");
3548        let b_id = EntityId::new("b");
3549        let common = g.common_neighbors(&a_id, &b_id).unwrap();
3550        assert_eq!(common.len(), 1);
3551        assert_eq!(common[0].id.as_str(), "shared");
3552    }
3553
3554    #[test]
3555    fn test_common_neighbors_empty_when_none_shared() {
3556        let g = GraphStore::new();
3557        g.add_entity(Entity::new("a", "A")).unwrap();
3558        g.add_entity(Entity::new("b", "B")).unwrap();
3559        g.add_entity(Entity::new("x", "X")).unwrap();
3560        g.add_entity(Entity::new("y", "Y")).unwrap();
3561        g.add_relationship(Relationship::new("a", "x", "e", 1.0)).unwrap();
3562        g.add_relationship(Relationship::new("b", "y", "e", 1.0)).unwrap();
3563        let a_id = EntityId::new("a");
3564        let b_id = EntityId::new("b");
3565        assert!(g.common_neighbors(&a_id, &b_id).unwrap().is_empty());
3566    }
3567
3568    // ── Round 3: entity_ids, is_empty, clear ─────────────────────────────────
3569
3570    #[test]
3571    fn test_graph_is_empty_initially() {
3572        let g = GraphStore::new();
3573        assert!(g.is_empty().unwrap());
3574    }
3575
3576    #[test]
3577    fn test_graph_is_empty_false_after_add() {
3578        let g = GraphStore::new();
3579        g.add_entity(Entity::new("a", "A")).unwrap();
3580        assert!(!g.is_empty().unwrap());
3581    }
3582
3583    #[test]
3584    fn test_graph_entity_ids_returns_all_ids() {
3585        let g = GraphStore::new();
3586        g.add_entity(Entity::new("x", "X")).unwrap();
3587        g.add_entity(Entity::new("y", "Y")).unwrap();
3588        let ids = g.entity_ids().unwrap();
3589        assert_eq!(ids.len(), 2);
3590        assert!(ids.iter().any(|id| id.0 == "x"));
3591        assert!(ids.iter().any(|id| id.0 == "y"));
3592    }
3593
3594    #[test]
3595    fn test_graph_clear_removes_entities_and_relationships() {
3596        let g = GraphStore::new();
3597        g.add_entity(Entity::new("a", "A")).unwrap();
3598        g.add_entity(Entity::new("b", "B")).unwrap();
3599        g.add_relationship(Relationship::new("a", "b", "links", 1.0))
3600        .unwrap();
3601        g.clear().unwrap();
3602        assert_eq!(g.entity_count().unwrap(), 0);
3603        assert_eq!(g.relationship_count().unwrap(), 0);
3604        assert!(g.is_empty().unwrap());
3605    }
3606
3607    // ── Round 16: weight_of, neighbors_in, path_exists ───────────────────────
3608
3609    #[test]
3610    fn test_weight_of_returns_edge_weight() {
3611        let g = make_graph();
3612        add(&g, "x"); add(&g, "y");
3613        link_w(&g, "x", "y", 3.5);
3614        let w = g.weight_of(&EntityId::new("x"), &EntityId::new("y")).unwrap();
3615        assert!(w.is_some());
3616        assert!((w.unwrap() - 3.5).abs() < 1e-6);
3617    }
3618
3619    #[test]
3620    fn test_weight_of_absent_edge_returns_none() {
3621        let g = make_graph();
3622        add(&g, "a"); add(&g, "b");
3623        let w = g.weight_of(&EntityId::new("a"), &EntityId::new("b")).unwrap();
3624        assert!(w.is_none());
3625    }
3626
3627    #[test]
3628    fn test_neighbors_in_returns_predecessors() {
3629        let g = make_graph();
3630        add(&g, "a"); add(&g, "b"); add(&g, "c");
3631        link(&g, "a", "c"); link(&g, "b", "c");
3632        let mut preds: Vec<String> = g
3633            .neighbors_in(&EntityId::new("c"))
3634            .unwrap()
3635            .into_iter()
3636            .map(|id| id.as_str().to_string())
3637            .collect();
3638        preds.sort();
3639        assert_eq!(preds, vec!["a", "b"]);
3640    }
3641
3642    #[test]
3643    fn test_neighbors_in_empty_for_node_with_no_incoming() {
3644        let g = make_graph();
3645        add(&g, "isolated");
3646        let preds = g.neighbors_in(&EntityId::new("isolated")).unwrap();
3647        assert!(preds.is_empty());
3648    }
3649
3650    #[test]
3651    fn test_path_exists_reachable() {
3652        let g = make_graph();
3653        add(&g, "s"); add(&g, "m"); add(&g, "t");
3654        link(&g, "s", "m"); link(&g, "m", "t");
3655        assert!(g.path_exists("s", "t").unwrap());
3656    }
3657
3658    #[test]
3659    fn test_path_exists_unreachable() {
3660        let g = make_graph();
3661        add(&g, "a"); add(&g, "b");
3662        assert!(!g.path_exists("a", "b").unwrap());
3663    }
3664
3665    // ── Round 5: GraphStore::neighbor_ids ─────────────────────────────────────
3666
3667    #[test]
3668    fn test_neighbor_ids_returns_direct_successors() {
3669        let g = make_graph();
3670        add(&g, "src"); add(&g, "dst1"); add(&g, "dst2");
3671        link(&g, "src", "dst1"); link(&g, "src", "dst2");
3672        let mut ids = g.neighbor_ids(&EntityId::new("src")).unwrap();
3673        ids.sort_by_key(|id| id.0.clone());
3674        assert_eq!(ids, vec![EntityId::new("dst1"), EntityId::new("dst2")]);
3675    }
3676
3677    #[test]
3678    fn test_neighbor_ids_empty_for_isolated_node() {
3679        let g = make_graph();
3680        add(&g, "isolated");
3681        let ids = g.neighbor_ids(&EntityId::new("isolated")).unwrap();
3682        assert!(ids.is_empty());
3683    }
3684
3685    // ── Round 17: density, all_entities, all_relationships, find_entities_by_label, bfs_bounded
3686
3687    #[test]
3688    fn test_density_zero_for_empty_graph() {
3689        let g = make_graph();
3690        assert_eq!(g.density().unwrap(), 0.0);
3691    }
3692
3693    #[test]
3694    fn test_density_zero_for_single_node() {
3695        let g = make_graph();
3696        add(&g, "solo");
3697        assert_eq!(g.density().unwrap(), 0.0);
3698    }
3699
3700    #[test]
3701    fn test_density_one_for_complete_directed_graph() {
3702        // 2 nodes with both directed edges → density = 2 / (2*1) = 1.0
3703        let g = make_graph();
3704        add(&g, "a"); add(&g, "b");
3705        link(&g, "a", "b"); link(&g, "b", "a");
3706        assert!((g.density().unwrap() - 1.0).abs() < 1e-9);
3707    }
3708
3709    #[test]
3710    fn test_density_partial() {
3711        // 3 nodes, 1 edge → max edges = 6, density = 1/6
3712        let g = make_graph();
3713        add(&g, "a"); add(&g, "b"); add(&g, "c");
3714        link(&g, "a", "b");
3715        let d = g.density().unwrap();
3716        assert!((d - 1.0/6.0).abs() < 1e-9);
3717    }
3718
3719    #[test]
3720    fn test_all_entities_returns_all_nodes() {
3721        let g = make_graph();
3722        add(&g, "x"); add(&g, "y"); add(&g, "z");
3723        assert_eq!(g.all_entities().unwrap().len(), 3);
3724    }
3725
3726    #[test]
3727    fn test_all_relationships_returns_all_edges() {
3728        let g = make_graph();
3729        add(&g, "a"); add(&g, "b"); add(&g, "c");
3730        link(&g, "a", "b"); link(&g, "b", "c");
3731        assert_eq!(g.all_relationships().unwrap().len(), 2);
3732    }
3733
3734    #[test]
3735    fn test_find_entities_by_label_returns_matches() {
3736        let g = make_graph();
3737        g.add_entity(Entity::new("n1", "Person")).unwrap();
3738        g.add_entity(Entity::new("n2", "Person")).unwrap();
3739        g.add_entity(Entity::new("n3", "Car")).unwrap();
3740        let people = g.find_entities_by_label("Person").unwrap();
3741        assert_eq!(people.len(), 2);
3742    }
3743
3744    #[test]
3745    fn test_bfs_bounded_limits_depth() {
3746        // a → b → c → d, bounded at depth 2 should only reach a,b,c
3747        let g = make_graph();
3748        add(&g, "a"); add(&g, "b"); add(&g, "c"); add(&g, "d");
3749        link(&g, "a", "b"); link(&g, "b", "c"); link(&g, "c", "d");
3750        let visited = g.bfs_bounded("a", 2, 100).unwrap();
3751        assert!(visited.contains(&EntityId::new("a")));
3752        assert!(visited.contains(&EntityId::new("b")));
3753        assert!(visited.contains(&EntityId::new("c")));
3754        assert!(!visited.contains(&EntityId::new("d")));
3755    }
3756
3757    // ── Round 18: avg_degree ─────────────────────────────────────────────────
3758
3759    #[test]
3760    fn test_avg_degree_zero_for_empty_graph() {
3761        let g = make_graph();
3762        assert_eq!(g.avg_degree().unwrap(), 0.0);
3763    }
3764
3765    #[test]
3766    fn test_avg_degree_correct_value() {
3767        // 3 nodes, 2 edges → 2/3
3768        let g = make_graph();
3769        add(&g, "a"); add(&g, "b"); add(&g, "c");
3770        link(&g, "a", "b"); link(&g, "a", "c");
3771        let d = g.avg_degree().unwrap();
3772        assert!((d - 2.0/3.0).abs() < 1e-9);
3773    }
3774
3775    // ── Round 19: total_weight, max/min_edge_weight ───────────────────────────
3776
3777    #[test]
3778    fn test_total_weight_zero_for_empty_graph() {
3779        let g = make_graph();
3780        assert_eq!(g.total_weight().unwrap(), 0.0);
3781    }
3782
3783    #[test]
3784    fn test_total_weight_sums_all_edges() {
3785        let g = make_graph();
3786        add(&g, "a"); add(&g, "b"); add(&g, "c");
3787        link_w(&g, "a", "b", 2.0); link_w(&g, "b", "c", 3.5);
3788        assert!((g.total_weight().unwrap() - 5.5).abs() < 1e-6);
3789    }
3790
3791    #[test]
3792    fn test_max_edge_weight_none_for_empty() {
3793        let g = make_graph();
3794        assert!(g.max_edge_weight().unwrap().is_none());
3795    }
3796
3797    #[test]
3798    fn test_max_edge_weight_returns_largest() {
3799        let g = make_graph();
3800        add(&g, "a"); add(&g, "b"); add(&g, "c");
3801        link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
3802        assert!((g.max_edge_weight().unwrap().unwrap() - 9.5).abs() < 1e-6);
3803    }
3804
3805    #[test]
3806    fn test_min_edge_weight_returns_smallest() {
3807        let g = make_graph();
3808        add(&g, "a"); add(&g, "b"); add(&g, "c");
3809        link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
3810        assert!((g.min_edge_weight().unwrap().unwrap() - 1.0).abs() < 1e-6);
3811    }
3812
3813    // ── Round 6: max_out_degree_entity / leaf_nodes ───────────────────────────
3814
3815    #[test]
3816    fn test_max_out_degree_entity_returns_node_with_most_edges() {
3817        let g = make_graph();
3818        add(&g, "hub"); add(&g, "a"); add(&g, "b"); add(&g, "leaf");
3819        link(&g, "hub", "a"); link(&g, "hub", "b"); link(&g, "a", "leaf");
3820        let best = g.max_out_degree_entity().unwrap().unwrap();
3821        assert_eq!(best.id, EntityId::new("hub"));
3822    }
3823
3824    #[test]
3825    fn test_max_out_degree_entity_none_for_empty_graph() {
3826        let g = make_graph();
3827        assert!(g.max_out_degree_entity().unwrap().is_none());
3828    }
3829
3830    #[test]
3831    fn test_leaf_nodes_returns_nodes_with_no_outgoing_edges() {
3832        let g = make_graph();
3833        add(&g, "root"); add(&g, "mid"); add(&g, "leaf1"); add(&g, "leaf2");
3834        link(&g, "root", "mid"); link(&g, "mid", "leaf1"); link(&g, "mid", "leaf2");
3835        let mut leaf_ids: Vec<String> = g
3836            .leaf_nodes()
3837            .unwrap()
3838            .into_iter()
3839            .map(|e| e.id.0.clone())
3840            .collect();
3841        leaf_ids.sort();
3842        assert_eq!(leaf_ids, vec!["leaf1", "leaf2"]);
3843    }
3844
3845    #[test]
3846    fn test_leaf_nodes_all_are_leaves_with_no_edges() {
3847        let g = make_graph();
3848        add(&g, "a"); add(&g, "b");
3849        assert_eq!(g.leaf_nodes().unwrap().len(), 2);
3850    }
3851
3852    // ── Round 7: top_n_by_out_degree / remove_entity_and_edges ───────────────
3853
3854    #[test]
3855    fn test_top_n_by_out_degree_returns_descending() {
3856        let g = make_graph();
3857        add(&g, "hub"); add(&g, "mid"); add(&g, "tip"); add(&g, "leaf");
3858        link(&g, "hub", "mid"); link(&g, "hub", "tip"); link(&g, "hub", "leaf");
3859        link(&g, "mid", "leaf");
3860        let top2 = g.top_n_by_out_degree(2).unwrap();
3861        assert_eq!(top2.len(), 2);
3862        assert_eq!(top2[0].id, EntityId::new("hub"));
3863    }
3864
3865    #[test]
3866    fn test_top_n_by_out_degree_zero_returns_empty() {
3867        let g = make_graph();
3868        add(&g, "x");
3869        assert!(g.top_n_by_out_degree(0).unwrap().is_empty());
3870    }
3871
3872    #[test]
3873    fn test_remove_entity_and_edges_removes_node_and_incident_edges() {
3874        let g = make_graph();
3875        add(&g, "a"); add(&g, "b"); add(&g, "c");
3876        link(&g, "a", "b"); link(&g, "b", "c");
3877        g.remove_entity_and_edges(&EntityId::new("b")).unwrap();
3878        assert_eq!(g.entity_count().unwrap(), 2);
3879        assert_eq!(g.relationship_count().unwrap(), 0);
3880    }
3881
3882    #[test]
3883    fn test_remove_entity_and_edges_errors_for_unknown_id() {
3884        let g = make_graph();
3885        let result = g.remove_entity_and_edges(&EntityId::new("ghost"));
3886        assert!(result.is_err());
3887    }
3888
3889    // ── Round 20: relationship_kinds / graph_density / EntityId::try_new ──────
3890
3891    #[test]
3892    fn test_relationship_kinds_returns_sorted_distinct_kinds() {
3893        let g = make_graph();
3894        add(&g, "a"); add(&g, "b"); add(&g, "c");
3895        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
3896        g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
3897        g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
3898        let kinds = g.relationship_kinds().unwrap();
3899        assert_eq!(kinds, vec!["KNOWS", "LIKES"]);
3900    }
3901
3902    #[test]
3903    fn test_relationship_kinds_empty_graph_returns_empty() {
3904        let g = make_graph();
3905        assert!(g.relationship_kinds().unwrap().is_empty());
3906    }
3907
3908    #[test]
3909    fn test_graph_density_zero_for_empty() {
3910        let g = make_graph();
3911        assert_eq!(g.graph_density().unwrap(), 0.0);
3912    }
3913
3914    #[test]
3915    fn test_graph_density_correct_for_partial_graph() {
3916        let g = make_graph();
3917        add(&g, "a"); add(&g, "b"); add(&g, "c");
3918        // 1 edge out of max 6 directed edges (3*2)
3919        link(&g, "a", "b");
3920        let d = g.graph_density().unwrap();
3921        assert!((d - 1.0 / 6.0).abs() < 1e-9);
3922    }
3923
3924    #[test]
3925    fn test_entity_id_try_new_rejects_empty() {
3926        let result = EntityId::try_new("");
3927        assert!(result.is_err());
3928    }
3929
3930    #[test]
3931    fn test_entity_id_try_new_accepts_nonempty() {
3932        let id = EntityId::try_new("valid").unwrap();
3933        assert_eq!(id.as_str(), "valid");
3934    }
3935
3936    // ── Round 8: hub_nodes / incident_relationships ───────────────────────────
3937
3938    #[test]
3939    fn test_hub_nodes_returns_nodes_meeting_threshold() {
3940        let g = make_graph();
3941        add(&g, "hub"); add(&g, "mid"); add(&g, "leaf");
3942        link(&g, "hub", "mid"); link(&g, "hub", "leaf"); link(&g, "mid", "leaf");
3943        let hubs = g.hub_nodes(2).unwrap();
3944        assert_eq!(hubs.len(), 1);
3945        assert_eq!(hubs[0].id, EntityId::new("hub"));
3946    }
3947
3948    #[test]
3949    fn test_hub_nodes_threshold_zero_returns_all() {
3950        let g = make_graph();
3951        add(&g, "a"); add(&g, "b");
3952        assert_eq!(g.hub_nodes(0).unwrap().len(), 2);
3953    }
3954
3955    #[test]
3956    fn test_incident_relationships_includes_outgoing_and_incoming() {
3957        let g = make_graph();
3958        add(&g, "a"); add(&g, "b"); add(&g, "c");
3959        link(&g, "a", "b"); link(&g, "c", "b");
3960        let rels = g.incident_relationships(&EntityId::new("b")).unwrap();
3961        assert_eq!(rels.len(), 2);
3962    }
3963
3964    #[test]
3965    fn test_incident_relationships_empty_for_isolated_node() {
3966        let g = make_graph();
3967        add(&g, "iso");
3968        assert!(g.incident_relationships(&EntityId::new("iso")).unwrap().is_empty());
3969    }
3970
3971    // ── Round 10: average_out_degree / in_degree_for ──────────────────────────
3972
3973    #[test]
3974    fn test_average_out_degree_empty_graph_is_zero() {
3975        let g = GraphStore::new();
3976        assert!((g.average_out_degree().unwrap() - 0.0).abs() < 1e-9);
3977    }
3978
3979    #[test]
3980    fn test_average_out_degree_two_nodes_one_edge() {
3981        let g = GraphStore::new();
3982        g.add_entity(Entity::new("a", "A")).unwrap();
3983        g.add_entity(Entity::new("b", "B")).unwrap();
3984        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3985        // 1 edge / 2 nodes = 0.5
3986        assert!((g.average_out_degree().unwrap() - 0.5).abs() < 1e-9);
3987    }
3988
3989    #[test]
3990    fn test_in_degree_for_counts_incoming_edges() {
3991        let g = GraphStore::new();
3992        g.add_entity(Entity::new("a", "A")).unwrap();
3993        g.add_entity(Entity::new("b", "B")).unwrap();
3994        g.add_entity(Entity::new("c", "C")).unwrap();
3995        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3996        g.add_relationship(Relationship::new("c", "b", "r", 1.0)).unwrap();
3997        assert_eq!(g.in_degree_for(&EntityId::new("b")).unwrap(), 2);
3998        assert_eq!(g.in_degree_for(&EntityId::new("a")).unwrap(), 0);
3999    }
4000
4001    #[test]
4002    fn test_in_degree_for_returns_zero_for_unknown_entity() {
4003        let g = GraphStore::new();
4004        assert_eq!(g.in_degree_for(&EntityId::new("ghost")).unwrap(), 0);
4005    }
4006
4007    // ── Round 12: EntityId::is_empty/starts_with, Entity::has_property, entity_labels, step_latency_p50/p99 ──
4008
4009    #[test]
4010    fn test_entity_id_is_empty_false_for_nonempty() {
4011        let id = EntityId::new("node-1");
4012        assert!(!id.is_empty());
4013    }
4014
4015    #[test]
4016    fn test_entity_id_starts_with_matches_prefix() {
4017        let id = EntityId::new("concept-42");
4018        assert!(id.starts_with("concept-"));
4019        assert!(!id.starts_with("entity-"));
4020    }
4021
4022    #[test]
4023    fn test_entity_id_starts_with_empty_always_true() {
4024        let id = EntityId::new("anything");
4025        assert!(id.starts_with(""));
4026    }
4027
4028    #[test]
4029    fn test_entity_has_property_returns_true_when_present() {
4030        let e = Entity::new("e", "Node")
4031            .with_property("color", serde_json::json!("blue"));
4032        assert!(e.has_property("color"));
4033        assert!(!e.has_property("size"));
4034    }
4035
4036    #[test]
4037    fn test_entity_has_property_false_when_no_properties() {
4038        let e = Entity::new("e", "Node");
4039        assert!(!e.has_property("any"));
4040    }
4041
4042    #[test]
4043    fn test_entity_labels_returns_distinct_sorted_labels() {
4044        let g = make_graph();
4045        g.add_entity(Entity::new("a", "Person")).unwrap();
4046        g.add_entity(Entity::new("b", "Concept")).unwrap();
4047        g.add_entity(Entity::new("c", "Person")).unwrap();
4048        let labels = g.entity_labels().unwrap();
4049        assert_eq!(labels, vec!["Concept", "Person"]);
4050    }
4051
4052    #[test]
4053    fn test_entity_labels_empty_for_empty_graph() {
4054        let g = make_graph();
4055        assert!(g.entity_labels().unwrap().is_empty());
4056    }
4057
4058    // ── Round 12: out_degree_for / predecessors / is_source ───────────────────
4059
4060    #[test]
4061    fn test_out_degree_for_returns_outgoing_edge_count() {
4062        let g = GraphStore::new();
4063        g.add_entity(Entity::new("a", "A")).unwrap();
4064        g.add_entity(Entity::new("b", "B")).unwrap();
4065        g.add_entity(Entity::new("c", "C")).unwrap();
4066        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4067        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4068        assert_eq!(g.out_degree_for(&EntityId::new("a")).unwrap(), 2);
4069        assert_eq!(g.out_degree_for(&EntityId::new("b")).unwrap(), 0);
4070    }
4071
4072    #[test]
4073    fn test_out_degree_for_returns_zero_for_unknown_entity() {
4074        let g = GraphStore::new();
4075        assert_eq!(g.out_degree_for(&EntityId::new("ghost")).unwrap(), 0);
4076    }
4077
4078    #[test]
4079    fn test_predecessors_returns_nodes_with_incoming_edges() {
4080        let g = GraphStore::new();
4081        g.add_entity(Entity::new("a", "A")).unwrap();
4082        g.add_entity(Entity::new("b", "B")).unwrap();
4083        g.add_entity(Entity::new("c", "C")).unwrap();
4084        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4085        g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
4086        let mut preds: Vec<String> = g
4087            .predecessors(&EntityId::new("c"))
4088            .unwrap()
4089            .iter()
4090            .map(|e| e.id.as_str().to_string())
4091            .collect();
4092        preds.sort();
4093        assert_eq!(preds, vec!["a", "b"]);
4094    }
4095
4096    #[test]
4097    fn test_predecessors_empty_for_source_node() {
4098        let g = GraphStore::new();
4099        g.add_entity(Entity::new("root", "Root")).unwrap();
4100        g.add_entity(Entity::new("child", "Child")).unwrap();
4101        g.add_relationship(Relationship::new("root", "child", "r", 1.0)).unwrap();
4102        assert!(g.predecessors(&EntityId::new("root")).unwrap().is_empty());
4103    }
4104
4105    #[test]
4106    fn test_is_source_true_for_node_with_no_incoming_edges() {
4107        let g = GraphStore::new();
4108        g.add_entity(Entity::new("src", "Src")).unwrap();
4109        g.add_entity(Entity::new("dst", "Dst")).unwrap();
4110        g.add_relationship(Relationship::new("src", "dst", "r", 1.0)).unwrap();
4111        assert!(g.is_source(&EntityId::new("src")).unwrap());
4112        assert!(!g.is_source(&EntityId::new("dst")).unwrap());
4113    }
4114
4115    // ── Round 13: Relationship::is_self_loop/reversed, find_entities_by_labels, remove_isolated ──
4116
4117    #[test]
4118    fn test_relationship_is_self_loop_true_when_from_equals_to() {
4119        let r = Relationship::new("a", "a", "self", 1.0);
4120        assert!(r.is_self_loop());
4121    }
4122
4123    #[test]
4124    fn test_relationship_is_self_loop_false_for_normal_edge() {
4125        let r = Relationship::new("a", "b", "edge", 1.0);
4126        assert!(!r.is_self_loop());
4127    }
4128
4129    #[test]
4130    fn test_relationship_reversed_swaps_endpoints() {
4131        let r = Relationship::new("from", "to", "knows", 0.5);
4132        let rev = r.reversed();
4133        assert_eq!(rev.from.as_str(), "to");
4134        assert_eq!(rev.to.as_str(), "from");
4135        assert_eq!(rev.kind, "knows");
4136        assert!((rev.weight - 0.5).abs() < 1e-6);
4137    }
4138
4139    #[test]
4140    fn test_find_entities_by_labels_returns_matching() {
4141        let g = make_graph();
4142        g.add_entity(Entity::new("p1", "Person")).unwrap();
4143        g.add_entity(Entity::new("p2", "Person")).unwrap();
4144        g.add_entity(Entity::new("c1", "Concept")).unwrap();
4145        let results = g.find_entities_by_labels(&["Person"]).unwrap();
4146        assert_eq!(results.len(), 2);
4147        assert!(results.iter().all(|e| e.label == "Person"));
4148    }
4149
4150    #[test]
4151    fn test_find_entities_by_labels_empty_when_no_match() {
4152        let g = make_graph();
4153        g.add_entity(Entity::new("n1", "Node")).unwrap();
4154        let results = g.find_entities_by_labels(&["Missing"]).unwrap();
4155        assert!(results.is_empty());
4156    }
4157
4158    #[test]
4159    fn test_remove_isolated_removes_nodes_without_edges() {
4160        let g = make_graph();
4161        g.add_entity(Entity::new("connected", "N")).unwrap();
4162        g.add_entity(Entity::new("isolated", "N")).unwrap();
4163        g.add_entity(Entity::new("other", "N")).unwrap();
4164        g.add_relationship(Relationship::new("connected", "other", "r", 1.0)).unwrap();
4165        let removed = g.remove_isolated().unwrap();
4166        assert_eq!(removed, 1);
4167        assert!(g.get_entity(&EntityId::new("isolated")).is_err());
4168        assert!(g.get_entity(&EntityId::new("connected")).is_ok());
4169    }
4170
4171    #[test]
4172    fn test_remove_isolated_zero_when_all_connected() {
4173        let g = make_graph();
4174        g.add_entity(Entity::new("a", "N")).unwrap();
4175        g.add_entity(Entity::new("b", "N")).unwrap();
4176        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4177        assert_eq!(g.remove_isolated().unwrap(), 0);
4178    }
4179
4180    // ── Round 13: successors / is_sink ────────────────────────────────────────
4181
4182    #[test]
4183    fn test_successors_returns_direct_out_neighbors() {
4184        let g = GraphStore::new();
4185        g.add_entity(Entity::new("a", "A")).unwrap();
4186        g.add_entity(Entity::new("b", "B")).unwrap();
4187        g.add_entity(Entity::new("c", "C")).unwrap();
4188        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4189        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4190        let mut ids: Vec<String> = g
4191            .successors(&EntityId::new("a"))
4192            .unwrap()
4193            .iter()
4194            .map(|e| e.id.as_str().to_string())
4195            .collect();
4196        ids.sort();
4197        assert_eq!(ids, vec!["b", "c"]);
4198    }
4199
4200    #[test]
4201    fn test_successors_empty_for_sink_node() {
4202        let g = GraphStore::new();
4203        g.add_entity(Entity::new("leaf", "L")).unwrap();
4204        assert!(g.successors(&EntityId::new("leaf")).unwrap().is_empty());
4205    }
4206
4207    #[test]
4208    fn test_is_sink_true_for_node_with_no_outgoing_edges() {
4209        let g = GraphStore::new();
4210        g.add_entity(Entity::new("a", "A")).unwrap();
4211        g.add_entity(Entity::new("b", "B")).unwrap();
4212        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4213        assert!(!g.is_sink(&EntityId::new("a")).unwrap());
4214        assert!(g.is_sink(&EntityId::new("b")).unwrap());
4215    }
4216
4217    #[test]
4218    fn test_is_sink_true_for_unknown_entity() {
4219        let g = GraphStore::new();
4220        assert!(g.is_sink(&EntityId::new("ghost")).unwrap());
4221    }
4222
4223    // ── Round 14: reachable_from / contains_cycle ─────────────────────────────
4224
4225    #[test]
4226    fn test_reachable_from_returns_all_downstream_nodes() {
4227        let g = GraphStore::new();
4228        g.add_entity(Entity::new("a", "N")).unwrap();
4229        g.add_entity(Entity::new("b", "N")).unwrap();
4230        g.add_entity(Entity::new("c", "N")).unwrap();
4231        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4232        g.add_relationship(Relationship::new("b", "c", "edge", 1.0)).unwrap();
4233        let reachable = g.reachable_from(&EntityId::new("a")).unwrap();
4234        assert!(reachable.contains(&EntityId::new("b")));
4235        assert!(reachable.contains(&EntityId::new("c")));
4236        assert!(!reachable.contains(&EntityId::new("a")));
4237    }
4238
4239    #[test]
4240    fn test_reachable_from_empty_for_sink_node() {
4241        let g = GraphStore::new();
4242        g.add_entity(Entity::new("sink", "N")).unwrap();
4243        let reachable = g.reachable_from(&EntityId::new("sink")).unwrap();
4244        assert!(reachable.is_empty());
4245    }
4246
4247    #[test]
4248    fn test_reachable_from_empty_for_unknown_node() {
4249        let g = GraphStore::new();
4250        let reachable = g.reachable_from(&EntityId::new("ghost")).unwrap();
4251        assert!(reachable.is_empty());
4252    }
4253
4254    #[test]
4255    fn test_contains_cycle_false_for_dag() {
4256        let g = GraphStore::new();
4257        g.add_entity(Entity::new("a", "N")).unwrap();
4258        g.add_entity(Entity::new("b", "N")).unwrap();
4259        g.add_entity(Entity::new("c", "N")).unwrap();
4260        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4261        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4262        assert!(!g.contains_cycle().unwrap());
4263    }
4264
4265    #[test]
4266    fn test_contains_cycle_true_for_cyclic_graph() {
4267        let g = GraphStore::new();
4268        g.add_entity(Entity::new("x", "N")).unwrap();
4269        g.add_entity(Entity::new("y", "N")).unwrap();
4270        g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
4271        g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
4272        assert!(g.contains_cycle().unwrap());
4273    }
4274
4275    #[test]
4276    fn test_contains_cycle_false_for_empty_graph() {
4277        let g = GraphStore::new();
4278        assert!(!g.contains_cycle().unwrap());
4279    }
4280
4281    // ── Round 15: GraphStore::is_acyclic ─────────────────────────────────────
4282
4283    #[test]
4284    fn test_is_acyclic_true_for_dag() {
4285        let g = GraphStore::new();
4286        g.add_entity(Entity::new("a", "N")).unwrap();
4287        g.add_entity(Entity::new("b", "N")).unwrap();
4288        g.add_entity(Entity::new("c", "N")).unwrap();
4289        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4290        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4291        assert!(g.is_acyclic().unwrap());
4292    }
4293
4294    #[test]
4295    fn test_is_acyclic_false_for_cyclic_graph() {
4296        let g = GraphStore::new();
4297        g.add_entity(Entity::new("x", "N")).unwrap();
4298        g.add_entity(Entity::new("y", "N")).unwrap();
4299        g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
4300        g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
4301        assert!(!g.is_acyclic().unwrap());
4302    }
4303
4304    #[test]
4305    fn test_is_acyclic_true_for_empty_graph() {
4306        let g = GraphStore::new();
4307        assert!(g.is_acyclic().unwrap());
4308    }
4309
4310    // ── Round 15: count_relationships_by_kind, merge, top_nodes_by_in/out_degree ──
4311
4312    #[test]
4313    fn test_count_relationships_by_kind_returns_correct_count() {
4314        let g = make_graph();
4315        g.add_entity(Entity::new("a", "N")).unwrap();
4316        g.add_entity(Entity::new("b", "N")).unwrap();
4317        g.add_entity(Entity::new("c", "N")).unwrap();
4318        g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
4319        g.add_relationship(Relationship::new("b", "c", "knows", 1.0)).unwrap();
4320        g.add_relationship(Relationship::new("a", "c", "likes", 0.5)).unwrap();
4321        assert_eq!(g.count_relationships_by_kind("knows").unwrap(), 2);
4322        assert_eq!(g.count_relationships_by_kind("likes").unwrap(), 1);
4323        assert_eq!(g.count_relationships_by_kind("absent").unwrap(), 0);
4324    }
4325
4326    #[test]
4327    fn test_merge_imports_entities_and_relationships() {
4328        let g1 = make_graph();
4329        g1.add_entity(Entity::new("a", "N")).unwrap();
4330        g1.add_entity(Entity::new("b", "N")).unwrap();
4331        g1.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4332
4333        let g2 = make_graph();
4334        g2.add_entity(Entity::new("c", "N")).unwrap();
4335        g2.add_entity(Entity::new("a", "N")).unwrap(); // duplicate — should not double-add
4336        g2.add_relationship(Relationship::new("c", "a", "s", 0.5)).unwrap();
4337
4338        g1.merge(&g2).unwrap();
4339        assert_eq!(g1.entity_count().unwrap(), 3); // a, b, c
4340        assert_eq!(g1.relationship_count().unwrap(), 2); // r + s
4341    }
4342
4343    #[test]
4344    fn test_top_nodes_by_in_degree_returns_sinks() {
4345        let g = make_graph();
4346        g.add_entity(Entity::new("hub", "N")).unwrap();
4347        g.add_entity(Entity::new("src1", "N")).unwrap();
4348        g.add_entity(Entity::new("src2", "N")).unwrap();
4349        g.add_relationship(Relationship::new("src1", "hub", "r", 1.0)).unwrap();
4350        g.add_relationship(Relationship::new("src2", "hub", "r", 1.0)).unwrap();
4351        let top = g.top_nodes_by_in_degree(1).unwrap();
4352        assert_eq!(top.len(), 1);
4353        assert_eq!(top[0].id.as_str(), "hub");
4354    }
4355
4356    #[test]
4357    fn test_top_nodes_by_out_degree_returns_sources() {
4358        let g = make_graph();
4359        g.add_entity(Entity::new("src", "N")).unwrap();
4360        g.add_entity(Entity::new("a", "N")).unwrap();
4361        g.add_entity(Entity::new("b", "N")).unwrap();
4362        g.add_relationship(Relationship::new("src", "a", "r", 1.0)).unwrap();
4363        g.add_relationship(Relationship::new("src", "b", "r", 1.0)).unwrap();
4364        let top = g.top_nodes_by_out_degree(1).unwrap();
4365        assert_eq!(top.len(), 1);
4366        assert_eq!(top[0].id.as_str(), "src");
4367    }
4368
4369    // ── Round 27: property_value, find_relationships_by_kind, count_relationships_by_kind ──
4370
4371    #[test]
4372    fn test_entity_property_value_returns_value() {
4373        let e = Entity::new("n1", "Node")
4374            .with_property("age", serde_json::Value::Number(42.into()));
4375        let val = e.property_value("age");
4376        assert!(val.is_some());
4377        assert_eq!(val.unwrap(), &serde_json::Value::Number(42.into()));
4378    }
4379
4380    #[test]
4381    fn test_entity_property_value_missing_returns_none() {
4382        let e = Entity::new("n1", "Node");
4383        assert!(e.property_value("missing").is_none());
4384    }
4385
4386    #[test]
4387    fn test_find_relationships_by_kind_returns_matching() {
4388        let g = GraphStore::new();
4389        g.add_entity(Entity::new("a", "N")).unwrap();
4390        g.add_entity(Entity::new("b", "N")).unwrap();
4391        g.add_entity(Entity::new("c", "N")).unwrap();
4392        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4393        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
4394        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
4395        let rels = g.find_relationships_by_kind("KNOWS").unwrap();
4396        assert_eq!(rels.len(), 2);
4397        assert!(rels.iter().all(|r| r.kind == "KNOWS"));
4398    }
4399
4400    #[test]
4401    fn test_find_relationships_by_kind_no_match_returns_empty() {
4402        let g = GraphStore::new();
4403        g.add_entity(Entity::new("a", "N")).unwrap();
4404        g.add_entity(Entity::new("b", "N")).unwrap();
4405        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4406        let rels = g.find_relationships_by_kind("HATES").unwrap();
4407        assert!(rels.is_empty());
4408    }
4409
4410    #[test]
4411    fn test_count_relationships_by_kind_correct() {
4412        let g = GraphStore::new();
4413        g.add_entity(Entity::new("a", "N")).unwrap();
4414        g.add_entity(Entity::new("b", "N")).unwrap();
4415        g.add_entity(Entity::new("c", "N")).unwrap();
4416        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4417        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
4418        g.add_relationship(Relationship::new("a", "c", "PART_OF", 1.0)).unwrap();
4419        assert_eq!(g.count_relationships_by_kind("KNOWS").unwrap(), 2);
4420        assert_eq!(g.count_relationships_by_kind("PART_OF").unwrap(), 1);
4421        assert_eq!(g.count_relationships_by_kind("MISSING").unwrap(), 0);
4422    }
4423
4424    // ── Round 16: max_out_degree / max_in_degree ──────────────────────────────
4425
4426    #[test]
4427    fn test_max_out_degree_returns_highest_out_degree() {
4428        let g = GraphStore::new();
4429        g.add_entity(Entity::new("a", "N")).unwrap();
4430        g.add_entity(Entity::new("b", "N")).unwrap();
4431        g.add_entity(Entity::new("c", "N")).unwrap();
4432        // a → b, a → c  (out-degree 2)
4433        // b → c          (out-degree 1)
4434        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4435        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4436        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4437        assert_eq!(g.max_out_degree().unwrap(), 2);
4438    }
4439
4440    #[test]
4441    fn test_max_out_degree_zero_for_empty_graph() {
4442        let g = GraphStore::new();
4443        assert_eq!(g.max_out_degree().unwrap(), 0);
4444    }
4445
4446    #[test]
4447    fn test_max_in_degree_returns_highest_in_degree() {
4448        let g = GraphStore::new();
4449        g.add_entity(Entity::new("a", "N")).unwrap();
4450        g.add_entity(Entity::new("b", "N")).unwrap();
4451        g.add_entity(Entity::new("c", "N")).unwrap();
4452        // a → c, b → c  (c has in-degree 2)
4453        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4454        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4455        assert_eq!(g.max_in_degree().unwrap(), 2);
4456    }
4457
4458    #[test]
4459    fn test_max_in_degree_zero_for_empty_graph() {
4460        let g = GraphStore::new();
4461        assert_eq!(g.max_in_degree().unwrap(), 0);
4462    }
4463
4464    // ── Round 17: sum_edge_weights ────────────────────────────────────────────
4465
4466    #[test]
4467    fn test_sum_edge_weights_correct_sum() {
4468        let g = GraphStore::new();
4469        g.add_entity(Entity::new("a", "N")).unwrap();
4470        g.add_entity(Entity::new("b", "N")).unwrap();
4471        g.add_entity(Entity::new("c", "N")).unwrap();
4472        g.add_relationship(Relationship::new("a", "b", "e", 1.5)).unwrap();
4473        g.add_relationship(Relationship::new("b", "c", "e", 2.5)).unwrap();
4474        let total = g.sum_edge_weights().unwrap();
4475        assert!((total - 4.0).abs() < 1e-9);
4476    }
4477
4478    #[test]
4479    fn test_sum_edge_weights_zero_for_empty_graph() {
4480        let g = GraphStore::new();
4481        assert!((g.sum_edge_weights().unwrap() - 0.0).abs() < 1e-9);
4482    }
4483
4484    // ── Round 22: weight_stats ────────────────────────────────────────────────
4485
4486    #[test]
4487    fn test_weight_stats_none_for_empty_graph() {
4488        let g = GraphStore::new();
4489        assert!(g.weight_stats().unwrap().is_none());
4490    }
4491
4492    #[test]
4493    fn test_weight_stats_returns_correct_min_max_mean() {
4494        let g = GraphStore::new();
4495        g.add_entity(Entity::new("a", "N")).unwrap();
4496        g.add_entity(Entity::new("b", "N")).unwrap();
4497        g.add_entity(Entity::new("c", "N")).unwrap();
4498        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4499        g.add_relationship(Relationship::new("b", "c", "e", 3.0)).unwrap();
4500        let (min, max, mean) = g.weight_stats().unwrap().unwrap();
4501        assert!((min - 1.0).abs() < 1e-9);
4502        assert!((max - 3.0).abs() < 1e-9);
4503        assert!((mean - 2.0).abs() < 1e-9);
4504    }
4505
4506    // ── Round 23: GraphStore::isolated_nodes ─────────────────────────────────
4507
4508    #[test]
4509    fn test_isolated_nodes_empty_graph_returns_empty_set() {
4510        let g = GraphStore::new();
4511        assert!(g.isolated_nodes().unwrap().is_empty());
4512    }
4513
4514    #[test]
4515    fn test_isolated_nodes_all_connected_returns_empty() {
4516        let g = GraphStore::new();
4517        g.add_entity(Entity::new("a", "N")).unwrap();
4518        g.add_entity(Entity::new("b", "N")).unwrap();
4519        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4520        // both a (out) and b (in) have an edge
4521        assert!(g.isolated_nodes().unwrap().is_empty());
4522    }
4523
4524    #[test]
4525    fn test_isolated_nodes_returns_orphan_entity() {
4526        let g = GraphStore::new();
4527        g.add_entity(Entity::new("a", "N")).unwrap();
4528        g.add_entity(Entity::new("b", "N")).unwrap();
4529        g.add_entity(Entity::new("orphan", "N")).unwrap();
4530        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4531        let iso = g.isolated_nodes().unwrap();
4532        assert_eq!(iso.len(), 1);
4533        assert!(iso.contains(&EntityId::new("orphan")));
4534    }
4535
4536    // ── Round 30: reverse, max_in_degree_entity, shortest_path_length ────────
4537
4538    #[test]
4539    fn test_reverse_flips_edge_direction() {
4540        let g = GraphStore::new();
4541        g.add_entity(Entity::new("x", "N")).unwrap();
4542        g.add_entity(Entity::new("y", "N")).unwrap();
4543        g.add_relationship(Relationship::new("x", "y", "edge", 1.0)).unwrap();
4544        let rev = g.reverse().unwrap();
4545        // In the reversed graph, y → x should exist
4546        let y_id = EntityId::new("y");
4547        let succs = rev.successors(&y_id).unwrap();
4548        assert!(succs.iter().any(|e| e.id == EntityId::new("x")));
4549    }
4550
4551    #[test]
4552    fn test_reverse_empty_graph_produces_empty_reverse() {
4553        let g = GraphStore::new();
4554        let rev = g.reverse().unwrap();
4555        assert!(rev.is_empty().unwrap());
4556    }
4557
4558    #[test]
4559    fn test_max_in_degree_entity_none_for_empty_graph() {
4560        let g = GraphStore::new();
4561        assert!(g.max_in_degree_entity().unwrap().is_none());
4562    }
4563
4564    #[test]
4565    fn test_max_in_degree_entity_returns_node_with_most_incoming() {
4566        let g = GraphStore::new();
4567        g.add_entity(Entity::new("hub", "N")).unwrap();
4568        g.add_entity(Entity::new("a", "N")).unwrap();
4569        g.add_entity(Entity::new("b", "N")).unwrap();
4570        g.add_relationship(Relationship::new("a", "hub", "e", 1.0)).unwrap();
4571        g.add_relationship(Relationship::new("b", "hub", "e", 1.0)).unwrap();
4572        let best = g.max_in_degree_entity().unwrap().unwrap();
4573        assert_eq!(best.id, EntityId::new("hub"));
4574    }
4575
4576    #[test]
4577    fn test_shortest_path_length_none_when_no_path() {
4578        let g = GraphStore::new();
4579        g.add_entity(Entity::new("a", "N")).unwrap();
4580        g.add_entity(Entity::new("b", "N")).unwrap();
4581        let len = g
4582            .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
4583            .unwrap();
4584        assert!(len.is_none());
4585    }
4586
4587    #[test]
4588    fn test_shortest_path_length_one_for_direct_edge() {
4589        let g = GraphStore::new();
4590        g.add_entity(Entity::new("a", "N")).unwrap();
4591        g.add_entity(Entity::new("b", "N")).unwrap();
4592        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4593        let len = g
4594            .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
4595            .unwrap();
4596        assert_eq!(len, Some(1));
4597    }
4598
4599    #[test]
4600    fn test_shortest_path_length_two_for_two_hop_path() {
4601        let g = GraphStore::new();
4602        g.add_entity(Entity::new("a", "N")).unwrap();
4603        g.add_entity(Entity::new("b", "N")).unwrap();
4604        g.add_entity(Entity::new("c", "N")).unwrap();
4605        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4606        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4607        let len = g
4608            .shortest_path_length(&EntityId::new("a"), &EntityId::new("c"))
4609            .unwrap();
4610        assert_eq!(len, Some(2));
4611    }
4612
4613    // ── Round 25: avg_out_degree / avg_in_degree ──────────────────────────────
4614
4615    #[test]
4616    fn test_avg_out_degree_zero_for_empty_graph() {
4617        let g = GraphStore::new();
4618        assert!((g.avg_out_degree().unwrap() - 0.0).abs() < 1e-9);
4619    }
4620
4621    #[test]
4622    fn test_avg_out_degree_correct_mean() {
4623        let g = GraphStore::new();
4624        g.add_entity(Entity::new("a", "N")).unwrap();
4625        g.add_entity(Entity::new("b", "N")).unwrap();
4626        g.add_entity(Entity::new("c", "N")).unwrap();
4627        // a → b, a → c (a has out-degree 2), b and c have 0
4628        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4629        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4630        // mean = (2 + 0 + 0) / 3 ≈ 0.667
4631        let avg = g.avg_out_degree().unwrap();
4632        assert!((avg - 2.0 / 3.0).abs() < 1e-9);
4633    }
4634
4635    #[test]
4636    fn test_avg_in_degree_zero_for_empty_graph() {
4637        let g = GraphStore::new();
4638        assert!((g.avg_in_degree().unwrap() - 0.0).abs() < 1e-9);
4639    }
4640
4641    #[test]
4642    fn test_avg_in_degree_correct_mean() {
4643        let g = GraphStore::new();
4644        g.add_entity(Entity::new("a", "N")).unwrap();
4645        g.add_entity(Entity::new("b", "N")).unwrap();
4646        g.add_entity(Entity::new("c", "N")).unwrap();
4647        // a → c, b → c (c has in-degree 2), a and b have 0
4648        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4649        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4650        // mean = (0 + 0 + 2) / 3 ≈ 0.667
4651        let avg = g.avg_in_degree().unwrap();
4652        assert!((avg - 2.0 / 3.0).abs() < 1e-9);
4653    }
4654
4655    // ── Round 26: has_entity ──────────────────────────────────────────────────
4656
4657    #[test]
4658    fn test_graph_store_has_entity_false_when_missing() {
4659        let g = GraphStore::new();
4660        let id = EntityId::new("nonexistent");
4661        assert!(!g.has_entity(&id).unwrap());
4662    }
4663
4664    #[test]
4665    fn test_graph_store_has_entity_true_after_add() {
4666        let g = GraphStore::new();
4667        g.add_entity(Entity::new("node-a", "Person")).unwrap();
4668        let id = EntityId::new("node-a");
4669        assert!(g.has_entity(&id).unwrap());
4670    }
4671
4672    // ── Round 32: Entity::with_property, GraphStore::remove_relationship,
4673    //             GraphStore::update_entity_property ─────────────────────────
4674
4675    #[test]
4676    fn test_entity_with_property_stores_value() {
4677        let e = Entity::new("e1", "Label")
4678            .with_property("score", serde_json::json!(42));
4679        assert_eq!(e.property_value("score"), Some(&serde_json::json!(42)));
4680    }
4681
4682    #[test]
4683    fn test_entity_with_property_chaining() {
4684        let e = Entity::new("e2", "L")
4685            .with_property("a", serde_json::json!(1))
4686            .with_property("b", serde_json::json!(2));
4687        assert!(e.has_property("a"));
4688        assert!(e.has_property("b"));
4689    }
4690
4691    #[test]
4692    fn test_remove_relationship_succeeds_when_exists() {
4693        let g = GraphStore::new();
4694        g.add_entity(Entity::new("x", "N")).unwrap();
4695        g.add_entity(Entity::new("y", "N")).unwrap();
4696        g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
4697        g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "link").unwrap();
4698        assert_eq!(g.relationship_count().unwrap(), 0);
4699    }
4700
4701    #[test]
4702    fn test_remove_relationship_errors_when_missing() {
4703        let g = GraphStore::new();
4704        g.add_entity(Entity::new("x", "N")).unwrap();
4705        g.add_entity(Entity::new("y", "N")).unwrap();
4706        let result = g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "ghost");
4707        assert!(result.is_err());
4708    }
4709
4710    #[test]
4711    fn test_update_entity_property_returns_true_when_entity_exists() {
4712        let g = GraphStore::new();
4713        g.add_entity(Entity::new("node", "N")).unwrap();
4714        let updated = g
4715            .update_entity_property(&EntityId::new("node"), "color", serde_json::json!("red"))
4716            .unwrap();
4717        assert!(updated);
4718        let entity = g.get_entity(&EntityId::new("node")).unwrap();
4719        assert_eq!(entity.property_value("color"), Some(&serde_json::json!("red")));
4720    }
4721
4722    #[test]
4723    fn test_update_entity_property_returns_false_for_unknown_entity() {
4724        let g = GraphStore::new();
4725        let updated = g
4726            .update_entity_property(&EntityId::new("ghost"), "key", serde_json::json!(1))
4727            .unwrap();
4728        assert!(!updated);
4729    }
4730
4731    // ── Round 27: has_any_entities ────────────────────────────────────────────
4732
4733    #[test]
4734    fn test_graph_store_has_any_entities_false_when_empty() {
4735        let g = GraphStore::new();
4736        assert!(!g.has_any_entities().unwrap());
4737    }
4738
4739    #[test]
4740    fn test_graph_store_has_any_entities_true_after_add() {
4741        let g = GraphStore::new();
4742        g.add_entity(Entity::new("x", "Node")).unwrap();
4743        assert!(g.has_any_entities().unwrap());
4744    }
4745
4746    // ── Round 28: entity_type_count ───────────────────────────────────────────
4747
4748    #[test]
4749    fn test_entity_type_count_zero_for_empty_graph() {
4750        let g = GraphStore::new();
4751        assert_eq!(g.entity_type_count().unwrap(), 0);
4752    }
4753
4754    #[test]
4755    fn test_entity_type_count_counts_distinct_labels() {
4756        let g = GraphStore::new();
4757        g.add_entity(Entity::new("a", "Person")).unwrap();
4758        g.add_entity(Entity::new("b", "Person")).unwrap();
4759        g.add_entity(Entity::new("c", "Concept")).unwrap();
4760        // "Person" and "Concept" → 2 distinct types
4761        assert_eq!(g.entity_type_count().unwrap(), 2);
4762    }
4763
4764    // ── Round 29: orphan_count ────────────────────────────────────────────────
4765
4766    #[test]
4767    fn test_orphan_count_zero_for_empty_graph() {
4768        let g = GraphStore::new();
4769        assert_eq!(g.orphan_count().unwrap(), 0);
4770    }
4771
4772    #[test]
4773    fn test_orphan_count_all_orphans_with_no_relationships() {
4774        let g = GraphStore::new();
4775        g.add_entity(Entity::new("a", "N")).unwrap();
4776        g.add_entity(Entity::new("b", "N")).unwrap();
4777        assert_eq!(g.orphan_count().unwrap(), 2);
4778    }
4779
4780    #[test]
4781    fn test_orphan_count_excludes_entities_with_edges() {
4782        let g = GraphStore::new();
4783        g.add_entity(Entity::new("a", "N")).unwrap();
4784        g.add_entity(Entity::new("b", "N")).unwrap();
4785        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4786        // "a" has out-edge → not orphan; "b" has no out-edges → orphan
4787        assert_eq!(g.orphan_count().unwrap(), 1);
4788    }
4789
4790    // ── Round 30: labels ──────────────────────────────────────────────────────
4791
4792    #[test]
4793    fn test_labels_empty_for_empty_graph() {
4794        let g = GraphStore::new();
4795        assert!(g.labels().unwrap().is_empty());
4796    }
4797
4798    #[test]
4799    fn test_labels_returns_distinct_sorted_labels() {
4800        let g = GraphStore::new();
4801        g.add_entity(Entity::new("a", "Concept")).unwrap();
4802        g.add_entity(Entity::new("b", "Person")).unwrap();
4803        g.add_entity(Entity::new("c", "Concept")).unwrap();
4804        assert_eq!(g.labels().unwrap(), vec!["Concept".to_string(), "Person".to_string()]);
4805    }
4806
4807    #[test]
4808    fn test_incoming_count_for_counts_inbound_edges() {
4809        let g = GraphStore::new();
4810        g.add_entity(Entity::new("a", "Node")).unwrap();
4811        g.add_entity(Entity::new("b", "Node")).unwrap();
4812        g.add_entity(Entity::new("c", "Node")).unwrap();
4813        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4814        g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
4815        assert_eq!(g.incoming_count_for(&EntityId::new("b")).unwrap(), 2);
4816        assert_eq!(g.incoming_count_for(&EntityId::new("a")).unwrap(), 0);
4817    }
4818
4819    #[test]
4820    fn test_outgoing_count_for_counts_outbound_edges() {
4821        let g = GraphStore::new();
4822        g.add_entity(Entity::new("a", "Node")).unwrap();
4823        g.add_entity(Entity::new("b", "Node")).unwrap();
4824        g.add_entity(Entity::new("c", "Node")).unwrap();
4825        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4826        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
4827        assert_eq!(g.outgoing_count_for(&EntityId::new("a")).unwrap(), 2);
4828        assert_eq!(g.outgoing_count_for(&EntityId::new("b")).unwrap(), 0);
4829    }
4830
4831    #[test]
4832    fn test_source_count_returns_number_of_nodes_with_no_incoming_edges() {
4833        let g = GraphStore::new();
4834        g.add_entity(Entity::new("a", "Node")).unwrap();
4835        g.add_entity(Entity::new("b", "Node")).unwrap();
4836        g.add_entity(Entity::new("c", "Node")).unwrap();
4837        // a→b, a→c: b and c have incoming edges; a has none
4838        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4839        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
4840        assert_eq!(g.source_count().unwrap(), 1);
4841    }
4842
4843    #[test]
4844    fn test_source_count_all_isolated_nodes_are_sources() {
4845        let g = GraphStore::new();
4846        g.add_entity(Entity::new("x", "Node")).unwrap();
4847        g.add_entity(Entity::new("y", "Node")).unwrap();
4848        assert_eq!(g.source_count().unwrap(), 2);
4849    }
4850
4851    #[test]
4852    fn test_sink_count_returns_nodes_with_no_outgoing_edges() {
4853        let g = GraphStore::new();
4854        g.add_entity(Entity::new("a", "Node")).unwrap();
4855        g.add_entity(Entity::new("b", "Node")).unwrap();
4856        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4857        // b has no outgoing edges → sink
4858        assert_eq!(g.sink_count().unwrap(), 1);
4859        assert_eq!(g.source_count().unwrap(), 1);
4860    }
4861
4862    #[test]
4863    fn test_has_self_loops_true_when_self_loop_exists() {
4864        let g = GraphStore::new();
4865        g.add_entity(Entity::new("a", "Node")).unwrap();
4866        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
4867        assert!(g.has_self_loops().unwrap());
4868    }
4869
4870    #[test]
4871    fn test_has_self_loops_false_when_no_self_loops() {
4872        let g = GraphStore::new();
4873        g.add_entity(Entity::new("a", "Node")).unwrap();
4874        g.add_entity(Entity::new("b", "Node")).unwrap();
4875        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4876        assert!(!g.has_self_loops().unwrap());
4877    }
4878
4879    #[test]
4880    fn test_bidirectional_count_counts_mutual_edges() {
4881        let g = GraphStore::new();
4882        g.add_entity(Entity::new("a", "Node")).unwrap();
4883        g.add_entity(Entity::new("b", "Node")).unwrap();
4884        g.add_entity(Entity::new("c", "Node")).unwrap();
4885        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4886        g.add_relationship(Relationship::new("b", "a", "link", 1.0)).unwrap(); // bidirectional pair
4887        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap(); // one-way
4888        assert_eq!(g.bidirectional_count().unwrap(), 1);
4889    }
4890
4891    #[test]
4892    fn test_bidirectional_count_zero_when_no_mutual_edges() {
4893        let g = GraphStore::new();
4894        g.add_entity(Entity::new("a", "Node")).unwrap();
4895        g.add_entity(Entity::new("b", "Node")).unwrap();
4896        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4897        assert_eq!(g.bidirectional_count().unwrap(), 0);
4898    }
4899
4900    // ── Round 36 ──────────────────────────────────────────────────────────────
4901
4902    #[test]
4903    fn test_relationship_kind_count_counts_distinct_kinds() {
4904        let g = GraphStore::new();
4905        g.add_entity(Entity::new("a", "N")).unwrap();
4906        g.add_entity(Entity::new("b", "N")).unwrap();
4907        g.add_entity(Entity::new("c", "N")).unwrap();
4908        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
4909        g.add_relationship(Relationship::new("b", "c", "friend", 1.0)).unwrap();
4910        g.add_relationship(Relationship::new("a", "c", "enemy", 1.0)).unwrap();
4911        assert_eq!(g.relationship_kind_count().unwrap(), 2);
4912    }
4913
4914    #[test]
4915    fn test_relationship_kind_count_zero_when_empty() {
4916        let g = GraphStore::new();
4917        assert_eq!(g.relationship_kind_count().unwrap(), 0);
4918    }
4919
4920    #[test]
4921    fn test_entities_with_self_loops_returns_self_loop_ids() {
4922        let g = GraphStore::new();
4923        g.add_entity(Entity::new("a", "N")).unwrap();
4924        g.add_entity(Entity::new("b", "N")).unwrap();
4925        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
4926        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4927        let ids = g.entities_with_self_loops().unwrap();
4928        assert_eq!(ids, vec![EntityId::new("a")]);
4929    }
4930
4931    #[test]
4932    fn test_entities_with_self_loops_empty_when_no_self_loops() {
4933        let g = GraphStore::new();
4934        g.add_entity(Entity::new("a", "N")).unwrap();
4935        g.add_entity(Entity::new("b", "N")).unwrap();
4936        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4937        assert!(g.entities_with_self_loops().unwrap().is_empty());
4938    }
4939
4940    // ── Round 37 ──────────────────────────────────────────────────────────────
4941
4942    #[test]
4943    fn test_isolated_entity_count_returns_count_with_no_edges() {
4944        let g = GraphStore::new();
4945        g.add_entity(Entity::new("a", "N")).unwrap();
4946        g.add_entity(Entity::new("b", "N")).unwrap();
4947        g.add_entity(Entity::new("c", "N")).unwrap();
4948        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4949        // c is isolated (no incoming, no outgoing)
4950        assert_eq!(g.isolated_entity_count().unwrap(), 1);
4951    }
4952
4953    #[test]
4954    fn test_isolated_entity_count_zero_when_all_connected() {
4955        let g = GraphStore::new();
4956        g.add_entity(Entity::new("a", "N")).unwrap();
4957        g.add_entity(Entity::new("b", "N")).unwrap();
4958        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4959        assert_eq!(g.isolated_entity_count().unwrap(), 0);
4960    }
4961
4962    #[test]
4963    fn test_avg_relationship_weight_returns_mean() {
4964        let g = GraphStore::new();
4965        g.add_entity(Entity::new("a", "N")).unwrap();
4966        g.add_entity(Entity::new("b", "N")).unwrap();
4967        g.add_entity(Entity::new("c", "N")).unwrap();
4968        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4969        g.add_relationship(Relationship::new("b", "c", "link", 3.0)).unwrap();
4970        let avg = g.avg_relationship_weight().unwrap();
4971        assert!((avg - 2.0).abs() < 1e-6);
4972    }
4973
4974    #[test]
4975    fn test_avg_relationship_weight_zero_when_no_relationships() {
4976        let g = GraphStore::new();
4977        assert_eq!(g.avg_relationship_weight().unwrap(), 0.0);
4978    }
4979
4980    // ── Round 38 ──────────────────────────────────────────────────────────────
4981
4982    #[test]
4983    fn test_total_in_degree_equals_relationship_count() {
4984        let g = GraphStore::new();
4985        g.add_entity(Entity::new("a", "N")).unwrap();
4986        g.add_entity(Entity::new("b", "N")).unwrap();
4987        g.add_entity(Entity::new("c", "N")).unwrap();
4988        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4989        g.add_relationship(Relationship::new("b", "c", "link", 1.0)).unwrap();
4990        assert_eq!(g.total_in_degree().unwrap(), 2);
4991    }
4992
4993    #[test]
4994    fn test_total_in_degree_zero_when_no_relationships() {
4995        let g = GraphStore::new();
4996        assert_eq!(g.total_in_degree().unwrap(), 0);
4997    }
4998
4999    #[test]
5000    fn test_relationship_count_between_counts_edges_between_pair() {
5001        let g = GraphStore::new();
5002        g.add_entity(Entity::new("a", "N")).unwrap();
5003        g.add_entity(Entity::new("b", "N")).unwrap();
5004        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
5005        g.add_relationship(Relationship::new("a", "b", "colleague", 1.0)).unwrap();
5006        let from = EntityId::new("a");
5007        let to = EntityId::new("b");
5008        assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 2);
5009    }
5010
5011    #[test]
5012    fn test_relationship_count_between_returns_zero_for_no_edge() {
5013        let g = GraphStore::new();
5014        g.add_entity(Entity::new("a", "N")).unwrap();
5015        g.add_entity(Entity::new("b", "N")).unwrap();
5016        let from = EntityId::new("a");
5017        let to = EntityId::new("b");
5018        assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 0);
5019    }
5020
5021    // ── Round 39 ──────────────────────────────────────────────────────────────
5022
5023    #[test]
5024    fn test_edges_from_returns_all_outgoing_relationships() {
5025        let g = GraphStore::new();
5026        g.add_entity(Entity::new("a", "N")).unwrap();
5027        g.add_entity(Entity::new("b", "N")).unwrap();
5028        g.add_entity(Entity::new("c", "N")).unwrap();
5029        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
5030        g.add_relationship(Relationship::new("a", "c", "enemy", 0.5)).unwrap();
5031        let edges = g.edges_from(&EntityId::new("a")).unwrap();
5032        assert_eq!(edges.len(), 2);
5033    }
5034
5035    #[test]
5036    fn test_edges_from_returns_empty_for_unknown_entity() {
5037        let g = GraphStore::new();
5038        assert!(g.edges_from(&EntityId::new("missing")).unwrap().is_empty());
5039    }
5040
5041    #[test]
5042    fn test_neighbors_of_returns_sorted_unique_targets() {
5043        let g = GraphStore::new();
5044        g.add_entity(Entity::new("a", "N")).unwrap();
5045        g.add_entity(Entity::new("b", "N")).unwrap();
5046        g.add_entity(Entity::new("c", "N")).unwrap();
5047        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
5048        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
5049        let neighbors = g.neighbors_of(&EntityId::new("a")).unwrap();
5050        assert_eq!(neighbors, vec![EntityId::new("b"), EntityId::new("c")]);
5051    }
5052
5053    #[test]
5054    fn test_neighbors_of_returns_empty_for_no_outgoing_edges() {
5055        let g = GraphStore::new();
5056        g.add_entity(Entity::new("a", "N")).unwrap();
5057        assert!(g.neighbors_of(&EntityId::new("a")).unwrap().is_empty());
5058    }
5059}