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
110impl From<String> for EntityId {
111    /// Create an `EntityId` from an owned `String`.
112    ///
113    /// Equivalent to [`EntityId::new`]; emits a `tracing::warn!` for empty strings.
114    fn from(s: String) -> Self {
115        Self::new(s)
116    }
117}
118
119impl From<&str> for EntityId {
120    /// Create an `EntityId` from a string slice.
121    ///
122    /// Equivalent to [`EntityId::new`]; emits a `tracing::warn!` for empty strings.
123    fn from(s: &str) -> Self {
124        Self::new(s)
125    }
126}
127
128impl std::str::FromStr for EntityId {
129    type Err = AgentRuntimeError;
130
131    /// Parse an `EntityId` from a string, returning an error if the string is empty.
132    ///
133    /// Unlike [`EntityId::new`] this is a validated constructor: empty strings are
134    /// rejected with `AgentRuntimeError::Graph` rather than silently warned about.
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        Self::try_new(s)
137    }
138}
139
140impl std::ops::Deref for EntityId {
141    type Target = str;
142
143    /// Dereference to the inner ID string slice.
144    ///
145    /// Allows `&entity_id` to coerce to `&str` transparently.
146    fn deref(&self) -> &Self::Target {
147        &self.0
148    }
149}
150
151// ── Entity ────────────────────────────────────────────────────────────────────
152
153/// A node in the knowledge graph.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Entity {
156    /// Unique identifier.
157    pub id: EntityId,
158    /// Human-readable label (e.g. "Person", "Concept").
159    pub label: String,
160    /// Arbitrary key-value properties.
161    pub properties: HashMap<String, Value>,
162}
163
164impl Entity {
165    /// Construct a new entity with no properties.
166    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
167        Self {
168            id: EntityId::new(id),
169            label: label.into(),
170            properties: HashMap::new(),
171        }
172    }
173
174    /// Construct a new entity with the given properties.
175    pub fn with_properties(
176        id: impl Into<String>,
177        label: impl Into<String>,
178        properties: HashMap<String, Value>,
179    ) -> Self {
180        Self {
181            id: EntityId::new(id),
182            label: label.into(),
183            properties,
184        }
185    }
186
187    /// Add a single key-value property, consuming and returning `self`.
188    ///
189    /// Allows fluent builder-style construction:
190    /// ```rust,ignore
191    /// let e = Entity::new("alice", "Person")
192    ///     .with_property("age", 30.into())
193    ///     .with_property("role", "engineer".into());
194    /// ```
195    pub fn with_property(mut self, key: impl Into<String>, value: Value) -> Self {
196        self.properties.insert(key.into(), value);
197        self
198    }
199
200    /// Return `true` if the entity has a property with the given key.
201    pub fn has_property(&self, key: &str) -> bool {
202        self.properties.contains_key(key)
203    }
204
205    /// Return a reference to the property value for `key`, or `None` if absent.
206    pub fn property_value(&self, key: &str) -> Option<&serde_json::Value> {
207        self.properties.get(key)
208    }
209
210    /// Remove the property with the given key, returning its previous value.
211    ///
212    /// Returns `None` if the key was not present.  Allows incremental
213    /// property pruning without needing to reconstruct the entire entity.
214    pub fn remove_property(&mut self, key: &str) -> Option<serde_json::Value> {
215        self.properties.remove(key)
216    }
217
218    /// Return the number of properties stored on this entity.
219    pub fn property_count(&self) -> usize {
220        self.properties.len()
221    }
222
223    /// Return `true` if this entity has no properties.
224    pub fn properties_is_empty(&self) -> bool {
225        self.properties.is_empty()
226    }
227
228    /// Return a sorted list of property keys for this entity.
229    ///
230    /// Useful for inspecting an entity's schema without cloning all values.
231    pub fn property_keys(&self) -> Vec<&str> {
232        let mut keys: Vec<&str> = self.properties.keys().map(|k| k.as_str()).collect();
233        keys.sort_unstable();
234        keys
235    }
236}
237
238impl std::fmt::Display for Entity {
239    /// Render as `"Entity[id='...', label='...', props=n]"`.
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        write!(
242            f,
243            "Entity[id='{}', label='{}', props={}]",
244            self.id,
245            self.label,
246            self.properties.len()
247        )
248    }
249}
250
251// ── Relationship ──────────────────────────────────────────────────────────────
252
253/// A directed, typed edge between two entities.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Relationship {
256    /// Source entity.
257    pub from: EntityId,
258    /// Target entity.
259    pub to: EntityId,
260    /// Relationship type label (e.g. "KNOWS", "PART_OF").
261    pub kind: String,
262    /// Optional weight for weighted-graph use cases.
263    pub weight: f32,
264}
265
266impl Relationship {
267    /// Construct a new relationship with the given kind and weight.
268    pub fn new(
269        from: impl Into<String>,
270        to: impl Into<String>,
271        kind: impl Into<String>,
272        weight: f32,
273    ) -> Self {
274        Self {
275            from: EntityId::new(from),
276            to: EntityId::new(to),
277            kind: kind.into(),
278            weight,
279        }
280    }
281
282    /// Return `true` if this relationship is a self-loop (`from == to`).
283    pub fn is_self_loop(&self) -> bool {
284        self.from == self.to
285    }
286
287    /// Return a new `Relationship` with `from` and `to` swapped.
288    ///
289    /// The `kind` and `weight` are preserved unchanged.
290    pub fn reversed(&self) -> Self {
291        Self {
292            from: self.to.clone(),
293            to: self.from.clone(),
294            kind: self.kind.clone(),
295            weight: self.weight,
296        }
297    }
298
299    /// Return a copy of this relationship with the weight changed to `w`.
300    ///
301    /// All other fields (`from`, `to`, `kind`) are cloned unchanged, making
302    /// this convenient for builder-style graph construction:
303    ///
304    /// ```rust
305    /// use llm_agent_runtime::graph::Relationship;
306    /// let rel = Relationship::new("a", "b", "KNOWS", 1.0).with_weight(0.5);
307    /// assert_eq!(rel.weight, 0.5);
308    /// ```
309    pub fn with_weight(self, w: f32) -> Self {
310        Self { weight: w, ..self }
311    }
312}
313
314impl std::fmt::Display for Relationship {
315    /// Render as `"from --kind(weight)--> to"`.
316    ///
317    /// For example: `"alice --KNOWS(1.00)--> bob"`.
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        write!(f, "{} --{}({:.2})--> {}", self.from, self.kind, self.weight, self.to)
320    }
321}
322
323// ── MemGraphError (mirrors upstream) ─────────────────────────────────────────
324
325/// Graph-specific errors, mirrors `mem-graph::MemGraphError`.
326#[derive(Debug, thiserror::Error)]
327pub enum MemGraphError {
328    /// The requested entity was not found.
329    #[error("Entity '{0}' not found")]
330    EntityNotFound(String),
331
332    /// A relationship between the two entities already exists with the same kind.
333    #[error("Relationship '{kind}' from '{from}' to '{to}' already exists")]
334    DuplicateRelationship {
335        /// Source entity ID.
336        from: String,
337        /// Target entity ID.
338        to: String,
339        /// Relationship kind label.
340        kind: String,
341    },
342
343    /// Generic internal error.
344    #[error("Graph internal error: {0}")]
345    Internal(String),
346}
347
348impl From<MemGraphError> for AgentRuntimeError {
349    fn from(e: MemGraphError) -> Self {
350        AgentRuntimeError::Graph(e.to_string())
351    }
352}
353
354// ── GraphStore ────────────────────────────────────────────────────────────────
355
356/// In-memory knowledge graph supporting entities, relationships, BFS/DFS,
357/// shortest-path, weighted shortest-path, and graph analytics.
358///
359/// ## Guarantees
360/// - Thread-safe via `Arc<Mutex<_>>`
361/// - BFS/DFS are non-recursive (stack-safe)
362/// - Shortest-path is hop-count based (BFS)
363/// - Weighted shortest-path uses Dijkstra's algorithm
364#[derive(Debug, Clone)]
365pub struct GraphStore {
366    inner: Arc<Mutex<GraphInner>>,
367}
368
369#[derive(Debug)]
370struct GraphInner {
371    entities: HashMap<EntityId, Entity>,
372    /// Flat list kept for full-edge iteration (degree/betweenness/label-propagation).
373    relationships: Vec<Relationship>,
374    /// Adjacency index: entity → outgoing relationships.
375    /// Kept in sync with `relationships` to give O(degree) neighbour lookup
376    /// in BFS/DFS/shortest-path without a full linear scan.
377    adjacency: HashMap<EntityId, Vec<Relationship>>,
378    /// Reverse adjacency index: entity → set of entities with edges pointing TO it.
379    /// Enables O(in-degree) `in_degree`, `source_nodes`, and `isolates` without a
380    /// full O(|E|) scan.
381    reverse_adjacency: HashMap<EntityId, Vec<EntityId>>,
382    /// Cached result of cycle detection. Invalidated on any mutation.
383    cycle_cache: Option<bool>,
384}
385
386impl GraphStore {
387    /// Create a new, empty graph store.
388    pub fn new() -> Self {
389        Self {
390            inner: Arc::new(Mutex::new(GraphInner {
391                entities: HashMap::new(),
392                relationships: Vec::new(),
393                adjacency: HashMap::new(),
394                reverse_adjacency: HashMap::new(),
395                cycle_cache: None,
396            })),
397        }
398    }
399
400    /// Add an entity to the graph.
401    ///
402    /// If an entity with the same ID already exists, it is replaced.
403    pub fn add_entity(&self, entity: Entity) -> Result<(), AgentRuntimeError> {
404        let mut inner = recover_lock(self.inner.lock(), "add_entity");
405        inner.cycle_cache = None;
406        // Ensure an adjacency entry exists even if the entity has no outgoing edges.
407        inner.adjacency.entry(entity.id.clone()).or_default();
408        inner.entities.insert(entity.id.clone(), entity);
409        Ok(())
410    }
411
412    /// Retrieve an entity by ID.
413    pub fn get_entity(&self, id: &EntityId) -> Result<Entity, AgentRuntimeError> {
414        let inner = recover_lock(self.inner.lock(), "get_entity");
415        inner
416            .entities
417            .get(id)
418            .cloned()
419            .ok_or_else(|| AgentRuntimeError::Graph(format!("entity '{}' not found", id.0)))
420    }
421
422    /// Return `true` if an entity with the given `id` exists in the graph.
423    pub fn has_entity(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
424        let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity");
425        Ok(inner.entities.contains_key(id))
426    }
427
428    /// Return `true` if the graph contains at least one entity.
429    pub fn has_any_entities(&self) -> Result<bool, AgentRuntimeError> {
430        let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_entities");
431        Ok(!inner.entities.is_empty())
432    }
433
434    /// Return the number of distinct entity label strings present in the graph.
435    pub fn entity_type_count(&self) -> Result<usize, AgentRuntimeError> {
436        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_type_count");
437        let types: std::collections::HashSet<&str> =
438            inner.entities.values().map(|e| e.label.as_str()).collect();
439        Ok(types.len())
440    }
441
442    /// Return the distinct entity labels present in the graph, sorted.
443    pub fn labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
444        let inner = recover_lock(self.inner.lock(), "GraphStore::labels");
445        let mut labels: Vec<String> = inner
446            .entities
447            .values()
448            .map(|e| e.label.clone())
449            .collect::<std::collections::HashSet<_>>()
450            .into_iter()
451            .collect();
452        labels.sort();
453        Ok(labels)
454    }
455
456    /// Return the number of incoming relationships for the given entity.
457    ///
458    /// Returns `0` if the entity has no in-edges or does not exist.
459    pub fn incoming_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
460        let inner = recover_lock(self.inner.lock(), "GraphStore::incoming_count_for");
461        Ok(inner.reverse_adjacency.get(id).map_or(0, |v| v.len()))
462    }
463
464    /// Return the number of outbound edges from the given entity.
465    ///
466    /// Returns `0` if the entity has no outgoing relationships.
467    pub fn outgoing_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
468        let inner = recover_lock(self.inner.lock(), "GraphStore::outgoing_count_for");
469        Ok(inner.adjacency.get(id).map_or(0, |v| v.len()))
470    }
471
472    /// Return the count of entities that have no outgoing edges (sink nodes).
473    pub fn sink_count(&self) -> Result<usize, AgentRuntimeError> {
474        let inner = recover_lock(self.inner.lock(), "GraphStore::sink_count");
475        let count = inner
476            .entities
477            .keys()
478            .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
479            .count();
480        Ok(count)
481    }
482
483    /// Return the count of entity pairs `(A, B)` where edges exist in both directions.
484    ///
485    /// Each bidirectional pair is counted once.
486    pub fn bidirectional_count(&self) -> Result<usize, AgentRuntimeError> {
487        let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_count");
488        let mut count = 0usize;
489        for (from, rels) in &inner.adjacency {
490            for rel in rels {
491                let to = &rel.to;
492                if to > from {
493                    // Only count when to > from to avoid double-counting
494                    if inner.adjacency.get(to).map_or(false, |v| v.iter().any(|r| &r.to == from)) {
495                        count += 1;
496                    }
497                }
498            }
499        }
500        Ok(count)
501    }
502
503    /// Return `true` if any relationship is a self-loop (`from == to`).
504    pub fn has_self_loops(&self) -> Result<bool, AgentRuntimeError> {
505        let inner = recover_lock(self.inner.lock(), "GraphStore::has_self_loops");
506        let found = inner.adjacency.iter().any(|(from, rels)| {
507            rels.iter().any(|r| &r.to == from)
508        });
509        Ok(found)
510    }
511
512    /// Return the count of entities that have no incoming edges (source nodes).
513    pub fn source_count(&self) -> Result<usize, AgentRuntimeError> {
514        let inner = recover_lock(self.inner.lock(), "GraphStore::source_count");
515        let count = inner
516            .entities
517            .keys()
518            .filter(|id| inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty()))
519            .count();
520        Ok(count)
521    }
522
523    /// Return the number of entities that have no outgoing relationships.
524    pub fn orphan_count(&self) -> Result<usize, AgentRuntimeError> {
525        let inner = recover_lock(self.inner.lock(), "GraphStore::orphan_count");
526        let count = inner
527            .entities
528            .keys()
529            .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
530            .count();
531        Ok(count)
532    }
533
534    /// Return the count of entities that have neither outgoing nor incoming relationships.
535    pub fn isolated_entity_count(&self) -> Result<usize, AgentRuntimeError> {
536        let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_entity_count");
537        let count = inner.entities.keys().filter(|id| {
538            inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
539                && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
540        }).count();
541        Ok(count)
542    }
543
544    /// Return the mean weight of all relationships in the graph.
545    ///
546    /// Returns `0.0` if no relationships have been added.
547    pub fn avg_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
548        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_relationship_weight");
549        if inner.relationships.is_empty() {
550            return Ok(0.0);
551        }
552        let total: f32 = inner.relationships.iter().map(|r| r.weight).sum();
553        Ok(total as f64 / inner.relationships.len() as f64)
554    }
555
556    /// Return the sum of in-degrees across all entities.
557    ///
558    /// Equal to the total number of relationships in the graph (each relationship
559    /// contributes 1 to the in-degree of its target entity).
560    pub fn total_in_degree(&self) -> Result<usize, AgentRuntimeError> {
561        let inner = recover_lock(self.inner.lock(), "GraphStore::total_in_degree");
562        Ok(inner.relationships.len())
563    }
564
565    /// Return the count of directed relationships from `from` to `to`.
566    ///
567    /// Returns `0` if no such relationships exist.
568    pub fn relationship_count_between(
569        &self,
570        from: &EntityId,
571        to: &EntityId,
572    ) -> Result<usize, AgentRuntimeError> {
573        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_between");
574        Ok(inner
575            .adjacency
576            .get(from)
577            .map_or(0, |rels| rels.iter().filter(|r| &r.to == to).count()))
578    }
579
580    /// Return all relationships originating from `id`, in insertion order.
581    ///
582    /// Returns an empty `Vec` if the entity has no outgoing relationships or does not exist.
583    pub fn edges_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
584        let inner = recover_lock(self.inner.lock(), "GraphStore::edges_from");
585        Ok(inner
586            .adjacency
587            .get(id)
588            .cloned()
589            .unwrap_or_default())
590    }
591
592    /// Return `true` if there is at least one directed edge from `from` to `to`.
593    ///
594    /// Returns `false` when either entity does not exist or when no direct
595    /// relationship exists between the two.  To test reachability over multiple
596    /// hops use [`reachable_from`].
597    ///
598    /// [`reachable_from`]: GraphStore::reachable_from
599    pub fn has_edge(
600        &self,
601        from: &EntityId,
602        to: &EntityId,
603    ) -> Result<bool, AgentRuntimeError> {
604        let inner = recover_lock(self.inner.lock(), "GraphStore::has_edge");
605        Ok(inner
606            .adjacency
607            .get(from)
608            .map_or(false, |rels| rels.iter().any(|r| &r.to == to)))
609    }
610
611    /// Return the `EntityId`s reachable from `id` in a single hop, sorted.
612    ///
613    /// Returns an empty `Vec` if the entity has no outgoing relationships or does not exist.
614    pub fn neighbors_of(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
615        let inner = recover_lock(self.inner.lock(), "GraphStore::neighbors_of");
616        let mut targets: Vec<EntityId> = inner
617            .adjacency
618            .get(id)
619            .map_or_else(Vec::new, |rels| rels.iter().map(|r| r.to.clone()).collect());
620        targets.sort_unstable();
621        targets.dedup();
622        Ok(targets)
623    }
624
625    /// Add a directed relationship between two existing entities.
626    ///
627    /// Both source and target entities must already exist in the graph.
628    pub fn add_relationship(&self, rel: Relationship) -> Result<(), AgentRuntimeError> {
629        let mut inner = recover_lock(self.inner.lock(), "add_relationship");
630
631        if !inner.entities.contains_key(&rel.from) {
632            return Err(AgentRuntimeError::Graph(format!(
633                "source entity '{}' not found",
634                rel.from.0
635            )));
636        }
637        if !inner.entities.contains_key(&rel.to) {
638            return Err(AgentRuntimeError::Graph(format!(
639                "target entity '{}' not found",
640                rel.to.0
641            )));
642        }
643
644        // Reject duplicate (from, to, kind) triples — the DuplicateRelationship
645        // error variant existed but was never raised, silently allowing duplicate
646        // edges that corrupt relationship_count() and BFS/DFS result counts.
647        let duplicate = inner
648            .relationships
649            .iter()
650            .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
651        if duplicate {
652            return Err(AgentRuntimeError::Graph(
653                MemGraphError::DuplicateRelationship {
654                    from: rel.from.0.clone(),
655                    to: rel.to.0.clone(),
656                    kind: rel.kind.clone(),
657                }
658                .to_string(),
659            ));
660        }
661
662        inner.cycle_cache = None;
663        // Keep adjacency in sync: add to outgoing list for rel.from.
664        inner
665            .adjacency
666            .entry(rel.from.clone())
667            .or_default()
668            .push(rel.clone());
669        // Keep reverse adjacency in sync: add rel.from to incoming list for rel.to.
670        inner
671            .reverse_adjacency
672            .entry(rel.to.clone())
673            .or_default()
674            .push(rel.from.clone());
675        inner.relationships.push(rel);
676        Ok(())
677    }
678
679    /// Remove a specific relationship by (from, to, kind).
680    ///
681    /// Returns `Err` if no matching relationship exists.
682    pub fn remove_relationship(
683        &self,
684        from: &EntityId,
685        to: &EntityId,
686        kind: &str,
687    ) -> Result<(), AgentRuntimeError> {
688        let mut inner = recover_lock(self.inner.lock(), "remove_relationship");
689
690        let before = inner.relationships.len();
691        inner
692            .relationships
693            .retain(|r| !(&r.from == from && &r.to == to && r.kind == kind));
694        if inner.relationships.len() == before {
695            return Err(AgentRuntimeError::Graph(format!(
696                "relationship '{kind}' from '{}' to '{}' not found",
697                from.0, to.0
698            )));
699        }
700
701        // Keep adjacency in sync.
702        if let Some(adj) = inner.adjacency.get_mut(from) {
703            adj.retain(|r| !(&r.to == to && r.kind == kind));
704        }
705        // Keep reverse adjacency in sync.
706        if let Some(rev) = inner.reverse_adjacency.get_mut(to) {
707            rev.retain(|src| src != from);
708        }
709
710        inner.cycle_cache = None;
711        Ok(())
712    }
713
714    /// Remove an entity and all relationships involving it.
715    pub fn remove_entity(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
716        let mut inner = recover_lock(self.inner.lock(), "remove_entity");
717
718        if inner.entities.remove(id).is_none() {
719            return Err(AgentRuntimeError::Graph(format!(
720                "entity '{}' not found",
721                id.0
722            )));
723        }
724        inner.cycle_cache = None;
725        inner.relationships.retain(|r| &r.from != id && &r.to != id);
726        // Remove outgoing edges for this entity and scrub it from others' lists.
727        inner.adjacency.remove(id);
728        for adj in inner.adjacency.values_mut() {
729            adj.retain(|r| &r.to != id);
730        }
731        // Remove incoming edges for this entity from the reverse adjacency index.
732        inner.reverse_adjacency.remove(id);
733        for rev in inner.reverse_adjacency.values_mut() {
734            rev.retain(|src| src != id);
735        }
736        Ok(())
737    }
738
739    /// Return all direct outgoing neighbours of the given entity (BFS layer 1).
740    ///
741    /// Uses the adjacency index for O(degree) lookup instead of O(|edges|).
742    fn neighbours(adjacency: &HashMap<EntityId, Vec<Relationship>>, id: &EntityId) -> Vec<EntityId> {
743        adjacency
744            .get(id)
745            .map(|rels| rels.iter().map(|r| r.to.clone()).collect())
746            .unwrap_or_default()
747    }
748
749    /// Breadth-first search starting from `start`.
750    ///
751    /// Returns entity IDs in BFS discovery order (not including the start node).
752    #[tracing::instrument(skip(self))]
753    pub fn bfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
754        let inner = recover_lock(self.inner.lock(), "bfs");
755
756        if !inner.entities.contains_key(start) {
757            return Err(AgentRuntimeError::Graph(format!(
758                "start entity '{}' not found",
759                start.0
760            )));
761        }
762
763        let mut visited: HashSet<EntityId> = HashSet::new();
764        let mut queue: VecDeque<EntityId> = VecDeque::new();
765        let mut result: Vec<EntityId> = Vec::new();
766
767        visited.insert(start.clone());
768        queue.push_back(start.clone());
769
770        while let Some(current) = queue.pop_front() {
771            let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, &current);
772            for neighbour in neighbours {
773                if visited.insert(neighbour.clone()) {
774                    result.push(neighbour.clone());
775                    queue.push_back(neighbour);
776                }
777            }
778        }
779
780        tracing::debug!("BFS visited {} nodes", result.len());
781        Ok(result)
782    }
783
784    /// Depth-first search starting from `start`.
785    ///
786    /// Returns entity IDs in DFS discovery order (not including the start node).
787    #[tracing::instrument(skip(self))]
788    pub fn dfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
789        let inner = recover_lock(self.inner.lock(), "dfs");
790
791        if !inner.entities.contains_key(start) {
792            return Err(AgentRuntimeError::Graph(format!(
793                "start entity '{}' not found",
794                start.0
795            )));
796        }
797
798        let mut visited: HashSet<EntityId> = HashSet::new();
799        let mut stack: Vec<EntityId> = Vec::new();
800        let mut result: Vec<EntityId> = Vec::new();
801
802        visited.insert(start.clone());
803        stack.push(start.clone());
804
805        while let Some(current) = stack.pop() {
806            let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, &current);
807            for neighbour in neighbours {
808                if visited.insert(neighbour.clone()) {
809                    result.push(neighbour.clone());
810                    stack.push(neighbour);
811                }
812            }
813        }
814
815        tracing::debug!("DFS visited {} nodes", result.len());
816        Ok(result)
817    }
818
819    /// Find the shortest path (by hop count) between `from` and `to`.
820    ///
821    /// # Returns
822    /// - `Some(path)` — ordered list of `EntityId`s from `from` to `to` (inclusive)
823    /// - `None` — no path exists
824    #[tracing::instrument(skip(self))]
825    pub fn shortest_path(
826        &self,
827        from: &EntityId,
828        to: &EntityId,
829    ) -> Result<Option<Vec<EntityId>>, AgentRuntimeError> {
830        let inner = recover_lock(self.inner.lock(), "shortest_path");
831
832        if !inner.entities.contains_key(from) {
833            return Err(AgentRuntimeError::Graph(format!(
834                "source entity '{}' not found",
835                from.0
836            )));
837        }
838        if !inner.entities.contains_key(to) {
839            return Err(AgentRuntimeError::Graph(format!(
840                "target entity '{}' not found",
841                to.0
842            )));
843        }
844
845        if from == to {
846            return Ok(Some(vec![from.clone()]));
847        }
848
849        // Item 5 — predecessor-map BFS; O(1) per enqueue instead of O(path_len).
850        let mut visited: HashSet<EntityId> = HashSet::new();
851        let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
852        let mut queue: VecDeque<EntityId> = VecDeque::new();
853
854        visited.insert(from.clone());
855        queue.push_back(from.clone());
856
857        while let Some(current) = queue.pop_front() {
858            for neighbour in Self::neighbours(&inner.adjacency, &current) {
859                if &neighbour == to {
860                    // Reconstruct path by following prev back from current.
861                    let mut path = vec![neighbour, current.clone()];
862                    let mut node = current;
863                    while let Some(p) = prev.get(&node) {
864                        path.push(p.clone());
865                        node = p.clone();
866                    }
867                    path.reverse();
868                    return Ok(Some(path));
869                }
870                if visited.insert(neighbour.clone()) {
871                    prev.insert(neighbour.clone(), current.clone());
872                    queue.push_back(neighbour);
873                }
874            }
875        }
876
877        Ok(None)
878    }
879
880    /// Find the shortest weighted path between `from` and `to` using Dijkstra's algorithm.
881    ///
882    /// Uses `Relationship::weight` as edge cost. Negative weights are not supported
883    /// and will cause this method to return an error.
884    ///
885    /// # Returns
886    /// - `Ok(Some((path, total_weight)))` — the shortest path and its total weight
887    /// - `Ok(None)` — no path exists between `from` and `to`
888    /// - `Err(...)` — either entity not found, or a negative edge weight was encountered
889    pub fn shortest_path_weighted(
890        &self,
891        from: &EntityId,
892        to: &EntityId,
893    ) -> Result<Option<(Vec<EntityId>, f32)>, AgentRuntimeError> {
894        let inner = recover_lock(self.inner.lock(), "shortest_path_weighted");
895
896        if !inner.entities.contains_key(from) {
897            return Err(AgentRuntimeError::Graph(format!(
898                "source entity '{}' not found",
899                from.0
900            )));
901        }
902        if !inner.entities.contains_key(to) {
903            return Err(AgentRuntimeError::Graph(format!(
904                "target entity '{}' not found",
905                to.0
906            )));
907        }
908
909        // Validate: no negative weights
910        for rel in &inner.relationships {
911            if rel.weight < 0.0 {
912                return Err(AgentRuntimeError::Graph(format!(
913                    "negative weight {:.4} on edge '{}' -> '{}'",
914                    rel.weight, rel.from.0, rel.to.0
915                )));
916            }
917        }
918
919        if from == to {
920            return Ok(Some((vec![from.clone()], 0.0)));
921        }
922
923        // Dijkstra using a max-heap with negated weights to simulate a min-heap.
924        // Heap entries: (negated_cost, node_id)
925        let mut dist: HashMap<EntityId, f32> = HashMap::new();
926        let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
927        // BinaryHeap is a max-heap; negate weights to get min-heap behaviour.
928        let mut heap: BinaryHeap<(OrdF32, EntityId)> = BinaryHeap::new();
929
930        dist.insert(from.clone(), 0.0);
931        heap.push((OrdF32(-0.0), from.clone()));
932
933        while let Some((OrdF32(neg_cost), current)) = heap.pop() {
934            let cost = -neg_cost;
935
936            // Skip stale entries.
937            if let Some(&best) = dist.get(&current) {
938                if cost > best {
939                    continue;
940                }
941            }
942
943            if &current == to {
944                // Reconstruct path in reverse.
945                let mut path = vec![to.clone()];
946                let mut node = to.clone();
947                while let Some(p) = prev.get(&node) {
948                    path.push(p.clone());
949                    node = p.clone();
950                }
951                path.reverse();
952                return Ok(Some((path, cost)));
953            }
954
955            // Use the adjacency index for O(degree) neighbour lookup instead of
956            // scanning all relationships.
957            if let Some(rels) = inner.adjacency.get(&current) {
958                for rel in rels {
959                    let next_cost = cost + rel.weight;
960                    let entry = dist.entry(rel.to.clone()).or_insert(f32::INFINITY);
961                    if next_cost < *entry {
962                        *entry = next_cost;
963                        prev.insert(rel.to.clone(), current.clone());
964                        heap.push((OrdF32(-next_cost), rel.to.clone()));
965                    }
966                }
967            }
968        }
969
970        Ok(None)
971    }
972
973    /// BFS that builds a `HashSet` directly (including `start`).
974    ///
975    /// Operates on a pre-locked `GraphInner` to avoid acquiring the mutex twice
976    /// and to skip the intermediate `Vec` allocation that `bfs()` produces.
977    fn bfs_into_set(inner: &GraphInner, start: &EntityId) -> HashSet<EntityId> {
978        let mut visited: HashSet<EntityId> = HashSet::new();
979        let mut queue: VecDeque<EntityId> = VecDeque::new();
980        visited.insert(start.clone());
981        queue.push_back(start.clone());
982        while let Some(current) = queue.pop_front() {
983            for neighbour in Self::neighbours(&inner.adjacency, &current) {
984                if visited.insert(neighbour.clone()) {
985                    queue.push_back(neighbour);
986                }
987            }
988        }
989        visited
990    }
991
992    /// Compute the transitive closure: all entities reachable from `start`
993    /// (including `start` itself).
994    ///
995    /// Uses a single lock acquisition and builds the result as a `HashSet`
996    /// directly, avoiding the intermediate `Vec` that would otherwise be
997    /// produced by delegating to [`bfs`].
998    ///
999    /// [`bfs`]: GraphStore::bfs
1000    pub fn transitive_closure(
1001        &self,
1002        start: &EntityId,
1003    ) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1004        let inner = recover_lock(self.inner.lock(), "transitive_closure");
1005        if !inner.entities.contains_key(start) {
1006            return Err(AgentRuntimeError::Graph(format!(
1007                "start entity '{}' not found",
1008                start.0
1009            )));
1010        }
1011        Ok(Self::bfs_into_set(&inner, start))
1012    }
1013
1014    /// Return the number of entities in the graph.
1015    pub fn entity_count(&self) -> Result<usize, AgentRuntimeError> {
1016        let inner = recover_lock(self.inner.lock(), "entity_count");
1017        Ok(inner.entities.len())
1018    }
1019
1020    /// Return the number of entities in the graph.
1021    ///
1022    /// Alias for [`entity_count`] using graph-theory terminology.
1023    ///
1024    /// [`entity_count`]: GraphStore::entity_count
1025    pub fn node_count(&self) -> Result<usize, AgentRuntimeError> {
1026        self.entity_count()
1027    }
1028
1029    /// Return the number of relationships in the graph.
1030    pub fn relationship_count(&self) -> Result<usize, AgentRuntimeError> {
1031        let inner = recover_lock(self.inner.lock(), "relationship_count");
1032        Ok(inner.relationships.len())
1033    }
1034
1035    /// Return the average out-degree across all entities.
1036    ///
1037    /// Returns `0.0` for an empty graph.  The result is the total number of
1038    /// directed edges divided by the number of nodes.
1039    pub fn average_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1040        let inner = recover_lock(self.inner.lock(), "average_out_degree");
1041        let n = inner.entities.len();
1042        if n == 0 {
1043            return Ok(0.0);
1044        }
1045        Ok(inner.relationships.len() as f64 / n as f64)
1046    }
1047
1048    /// Return the in-degree (number of incoming edges) for the given entity.
1049    ///
1050    /// Returns `0` if the entity does not exist or has no incoming edges.
1051    pub fn in_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
1052        let inner = recover_lock(self.inner.lock(), "in_degree_for");
1053        Ok(inner
1054            .reverse_adjacency
1055            .get(entity_id)
1056            .map(|v| v.len())
1057            .unwrap_or(0))
1058    }
1059
1060    /// Return the out-degree (number of outgoing edges) for the given entity.
1061    ///
1062    /// Returns `0` if the entity does not exist or has no outgoing edges.
1063    pub fn out_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
1064        let inner = recover_lock(self.inner.lock(), "out_degree_for");
1065        Ok(inner
1066            .adjacency
1067            .get(entity_id)
1068            .map(|v| v.len())
1069            .unwrap_or(0))
1070    }
1071
1072    /// Return all entities that have a directed edge pointing **to** `entity_id`
1073    /// (its predecessors in the directed graph).
1074    ///
1075    /// Returns an empty `Vec` if the entity has no incoming edges or does not
1076    /// exist.
1077    pub fn predecessors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
1078        let inner = recover_lock(self.inner.lock(), "predecessors");
1079        let ids = inner
1080            .reverse_adjacency
1081            .get(entity_id)
1082            .cloned()
1083            .unwrap_or_default();
1084        Ok(ids
1085            .iter()
1086            .filter_map(|id| inner.entities.get(id).cloned())
1087            .collect())
1088    }
1089
1090    /// Return `true` if the entity has no incoming edges (in-degree == 0).
1091    ///
1092    /// In a DAG, source nodes (roots) have no predecessors.  Returns `false`
1093    /// if the entity does not exist.
1094    pub fn is_source(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
1095        Ok(self.in_degree_for(entity_id)? == 0)
1096    }
1097
1098    /// Return all entities directly reachable from `entity_id` via outgoing edges
1099    /// (its successors / immediate out-neighbors).
1100    ///
1101    /// Returns an empty `Vec` if the entity has no outgoing edges or does not exist.
1102    pub fn successors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
1103        let inner = recover_lock(self.inner.lock(), "successors");
1104        let rels = inner.adjacency.get(entity_id).cloned().unwrap_or_default();
1105        Ok(rels
1106            .iter()
1107            .filter_map(|r| inner.entities.get(&r.to).cloned())
1108            .collect())
1109    }
1110
1111    /// Return `true` if the entity has no outgoing edges (out-degree == 0).
1112    ///
1113    /// In a DAG, sink nodes (leaves) have no successors.  Returns `true` if
1114    /// the entity does not exist (unknown nodes cannot have outgoing edges).
1115    pub fn is_sink(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
1116        Ok(self.out_degree_for(entity_id)? == 0)
1117    }
1118
1119    /// Return the set of all entity IDs reachable from `start` via directed edges
1120    /// (BFS traversal).  The starting node itself is **not** included.
1121    ///
1122    /// Returns an empty `HashSet` if `start` has no outgoing edges or does not exist.
1123    pub fn reachable_from(
1124        &self,
1125        start: &EntityId,
1126    ) -> Result<std::collections::HashSet<EntityId>, AgentRuntimeError> {
1127        let inner = recover_lock(self.inner.lock(), "reachable_from");
1128        let mut visited = std::collections::HashSet::new();
1129        let mut queue = std::collections::VecDeque::new();
1130        if let Some(rels) = inner.adjacency.get(start) {
1131            for r in rels {
1132                if visited.insert(r.to.clone()) {
1133                    queue.push_back(r.to.clone());
1134                }
1135            }
1136        }
1137        while let Some(current) = queue.pop_front() {
1138            if let Some(rels) = inner.adjacency.get(&current) {
1139                for r in rels {
1140                    if visited.insert(r.to.clone()) {
1141                        queue.push_back(r.to.clone());
1142                    }
1143                }
1144            }
1145        }
1146        Ok(visited)
1147    }
1148
1149    /// Return `true` if the directed graph contains at least one cycle.
1150    ///
1151    /// Uses iterative DFS with a three-colour scheme (unvisited / in-stack / done).
1152    /// An empty graph is acyclic.
1153    pub fn contains_cycle(&self) -> Result<bool, AgentRuntimeError> {
1154        let inner = recover_lock(self.inner.lock(), "contains_cycle");
1155        let mut color: HashMap<&EntityId, u8> = HashMap::new();
1156
1157        for start in inner.entities.keys() {
1158            if color.get(start).copied().unwrap_or(0) != 0 {
1159                continue;
1160            }
1161            let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1162            color.insert(start, 1);
1163            while let Some((node, idx)) = stack.last_mut() {
1164                let neighbors = inner.adjacency.get(node).map(|v| v.as_slice()).unwrap_or(&[]);
1165                if *idx < neighbors.len() {
1166                    let neighbor = &neighbors[*idx].to;
1167                    *idx += 1;
1168                    match color.get(neighbor).copied().unwrap_or(0) {
1169                        1 => return Ok(true),
1170                        0 => {
1171                            color.insert(neighbor, 1);
1172                            stack.push((neighbor, 0));
1173                        }
1174                        _ => {}
1175                    }
1176                } else {
1177                    color.insert(*node, 2);
1178                    stack.pop();
1179                }
1180            }
1181        }
1182        Ok(false)
1183    }
1184
1185    /// Return the number of relationships in the graph.
1186    ///
1187    /// Alias for [`relationship_count`] using graph-theory terminology.
1188    ///
1189    /// [`relationship_count`]: GraphStore::relationship_count
1190    pub fn edge_count(&self) -> Result<usize, AgentRuntimeError> {
1191        self.relationship_count()
1192    }
1193
1194    /// Return `true` if the directed graph contains **no** cycles.
1195    ///
1196    /// Shorthand for `!self.contains_cycle()`.
1197    pub fn is_acyclic(&self) -> Result<bool, AgentRuntimeError> {
1198        Ok(!self.contains_cycle()?)
1199    }
1200
1201    /// Return the mean out-degree across all entities.
1202    ///
1203    /// Returns `0.0` for graphs with no entities.
1204    pub fn avg_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1205        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_out_degree");
1206        let n = inner.entities.len();
1207        if n == 0 {
1208            return Ok(0.0);
1209        }
1210        let total: usize = inner.entities.keys().map(|id| {
1211            inner.adjacency.get(id).map_or(0, |v| v.len())
1212        }).sum();
1213        Ok(total as f64 / n as f64)
1214    }
1215
1216    /// Return the mean in-degree across all entities.
1217    ///
1218    /// Returns `0.0` for graphs with no entities.
1219    pub fn avg_in_degree(&self) -> Result<f64, AgentRuntimeError> {
1220        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_in_degree");
1221        let n = inner.entities.len();
1222        if n == 0 {
1223            return Ok(0.0);
1224        }
1225        let total: usize = inner.entities.keys().map(|id| {
1226            inner.reverse_adjacency.get(id).map_or(0, |v| v.len())
1227        }).sum();
1228        Ok(total as f64 / n as f64)
1229    }
1230
1231    /// Return the maximum out-degree across all entities, or `0` for empty graphs.
1232    pub fn max_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1233        let inner = recover_lock(self.inner.lock(), "max_out_degree");
1234        Ok(inner
1235            .adjacency
1236            .values()
1237            .map(|v| v.len())
1238            .max()
1239            .unwrap_or(0))
1240    }
1241
1242    /// Return the maximum in-degree across all entities, or `0` for empty graphs.
1243    pub fn max_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1244        let inner = recover_lock(self.inner.lock(), "max_in_degree");
1245        Ok(inner
1246            .reverse_adjacency
1247            .values()
1248            .map(|v| v.len())
1249            .max()
1250            .unwrap_or(0))
1251    }
1252
1253    /// Return the minimum out-degree across all entities, or `0` for empty graphs.
1254    ///
1255    /// This counts only entities that are registered in the graph.  An entity
1256    /// with no outgoing edges has out-degree 0 and contributes to the minimum.
1257    pub fn min_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1258        let inner = recover_lock(self.inner.lock(), "min_out_degree");
1259        Ok(inner
1260            .adjacency
1261            .values()
1262            .map(|v| v.len())
1263            .min()
1264            .unwrap_or(0))
1265    }
1266
1267    /// Return the minimum in-degree across all entities, or `0` for empty graphs.
1268    ///
1269    /// An entity with no incoming edges has in-degree 0 and contributes to the
1270    /// minimum.  Source nodes (those with no predecessors) always have in-degree 0.
1271    pub fn min_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1272        let inner = recover_lock(self.inner.lock(), "min_in_degree");
1273        // Count in-degree from the reverse adjacency index, but also include
1274        // entities whose in-degree is 0 (they may be absent from reverse_adjacency).
1275        let min_from_reverse = inner
1276            .reverse_adjacency
1277            .values()
1278            .map(|v| v.len())
1279            .min()
1280            .unwrap_or(0);
1281        // If any entity has no entry in reverse_adjacency its in-degree is 0.
1282        let has_zero_in_degree = inner
1283            .entities
1284            .keys()
1285            .any(|id| !inner.reverse_adjacency.contains_key(id));
1286        if has_zero_in_degree {
1287            Ok(0)
1288        } else {
1289            Ok(min_from_reverse)
1290        }
1291    }
1292
1293    /// Return the distinct relationship kinds that originate **from** `id`,
1294    /// sorted lexicographically.
1295    ///
1296    /// Returns an empty `Vec` if the entity has no outgoing edges or does not
1297    /// exist.
1298    pub fn relationship_kinds_from(
1299        &self,
1300        id: &EntityId,
1301    ) -> Result<Vec<String>, AgentRuntimeError> {
1302        let inner = recover_lock(self.inner.lock(), "relationship_kinds_from");
1303        let mut kinds: Vec<String> = inner
1304            .adjacency
1305            .get(id)
1306            .map(|rels| {
1307                rels.iter()
1308                    .map(|r| r.kind.clone())
1309                    .collect::<std::collections::HashSet<_>>()
1310                    .into_iter()
1311                    .collect()
1312            })
1313            .unwrap_or_default();
1314        kinds.sort_unstable();
1315        Ok(kinds)
1316    }
1317
1318    /// Return `(min_weight, max_weight, mean_weight)` across all relationships.
1319    ///
1320    /// Returns `None` if the graph has no relationships.
1321    pub fn weight_stats(&self) -> Result<Option<(f64, f64, f64)>, AgentRuntimeError> {
1322        let inner = recover_lock(self.inner.lock(), "weight_stats");
1323        let weights: Vec<f64> = inner
1324            .adjacency
1325            .values()
1326            .flat_map(|rels| rels.iter())
1327            .map(|r| r.weight as f64)
1328            .collect();
1329        if weights.is_empty() {
1330            return Ok(None);
1331        }
1332        let min = weights.iter().cloned().fold(f64::INFINITY, f64::min);
1333        let max = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1334        let mean = weights.iter().sum::<f64>() / weights.len() as f64;
1335        Ok(Some((min, max, mean)))
1336    }
1337
1338    /// Return the set of entity IDs that have no inbound **and** no outbound edges.
1339    ///
1340    /// An isolated node is one that does not appear as a `from` or `to` endpoint
1341    /// in any relationship.  Useful for detecting orphaned entities.
1342    pub fn isolated_nodes(&self) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1343        let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_nodes");
1344        let mut result = HashSet::new();
1345        for id in inner.entities.keys() {
1346            let has_outbound = inner
1347                .adjacency
1348                .get(id)
1349                .map_or(false, |v| !v.is_empty());
1350            let has_inbound = inner
1351                .reverse_adjacency
1352                .get(id)
1353                .map_or(false, |v| !v.is_empty());
1354            if !has_outbound && !has_inbound {
1355                result.insert(id.clone());
1356            }
1357        }
1358        Ok(result)
1359    }
1360
1361    /// Return the sum of all relationship weights in the graph.
1362    ///
1363    /// Returns `0.0` for graphs with no relationships.
1364    pub fn sum_edge_weights(&self) -> Result<f64, AgentRuntimeError> {
1365        let inner = recover_lock(self.inner.lock(), "sum_edge_weights");
1366        Ok(inner
1367            .adjacency
1368            .values()
1369            .flat_map(|rels| rels.iter())
1370            .map(|r| r.weight as f64)
1371            .sum())
1372    }
1373
1374    /// Return all entity IDs in the graph without allocating full `Entity` objects.
1375    ///
1376    /// Cheaper than [`all_entities`] when only IDs are needed.
1377    ///
1378    /// [`all_entities`]: GraphStore::all_entities
1379    pub fn entity_ids(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1380        let inner = recover_lock(self.inner.lock(), "entity_ids");
1381        Ok(inner.entities.keys().cloned().collect())
1382    }
1383
1384    /// Return `true` if the graph contains no entities.
1385    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1386        Ok(self.entity_count()? == 0)
1387    }
1388
1389    /// Remove all entities and relationships from the graph.
1390    ///
1391    /// After this call `entity_count()` and `relationship_count()` both return `0`.
1392    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
1393        let mut inner = recover_lock(self.inner.lock(), "GraphStore::clear");
1394        inner.entities.clear();
1395        inner.relationships.clear();
1396        inner.adjacency.clear();
1397        inner.reverse_adjacency.clear();
1398        inner.cycle_cache = None;
1399        Ok(())
1400    }
1401
1402    /// Count entities whose label equals `label` (case-sensitive).
1403    pub fn entity_count_by_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
1404        let inner = recover_lock(self.inner.lock(), "entity_count_by_label");
1405        Ok(inner.entities.values().filter(|e| e.label == label).count())
1406    }
1407
1408    /// Compute the directed graph density: |E| / (|V| × (|V| − 1)).
1409    ///
1410    /// Returns `0.0` for graphs with fewer than two entities (no edges possible).
1411    /// A density of `1.0` means every possible directed edge is present.
1412    pub fn graph_density(&self) -> Result<f64, AgentRuntimeError> {
1413        let inner = recover_lock(self.inner.lock(), "graph_density");
1414        let v = inner.entities.len();
1415        if v < 2 {
1416            return Ok(0.0);
1417        }
1418        let e = inner.relationships.len() as f64;
1419        Ok(e / (v as f64 * (v - 1) as f64))
1420    }
1421
1422    /// Return all distinct entity label strings present in the graph, sorted.
1423    pub fn entity_labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
1424        let inner = recover_lock(self.inner.lock(), "entity_labels");
1425        let mut labels: Vec<String> = inner
1426            .entities
1427            .values()
1428            .map(|e| e.label.clone())
1429            .collect::<std::collections::HashSet<_>>()
1430            .into_iter()
1431            .collect();
1432        labels.sort_unstable();
1433        Ok(labels)
1434    }
1435
1436    /// Return all distinct relationship kind strings present in the graph, sorted.
1437    pub fn relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
1438        let inner = recover_lock(self.inner.lock(), "relationship_kinds");
1439        let mut kinds: Vec<String> = inner
1440            .relationships
1441            .iter()
1442            .map(|r| r.kind.clone())
1443            .collect::<std::collections::HashSet<_>>()
1444            .into_iter()
1445            .collect();
1446        kinds.sort_unstable();
1447        Ok(kinds)
1448    }
1449
1450    /// Return the number of distinct relationship kind strings present in the graph.
1451    pub fn relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
1452        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_kind_count");
1453        let count = inner
1454            .relationships
1455            .iter()
1456            .map(|r| r.kind.as_str())
1457            .collect::<std::collections::HashSet<_>>()
1458            .len();
1459        Ok(count)
1460    }
1461
1462    /// Return the `EntityId`s of entities that have at least one self-loop relationship.
1463    ///
1464    /// A self-loop is a relationship where `from == to`.
1465    pub fn entities_with_self_loops(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1466        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_self_loops");
1467        let mut ids: Vec<EntityId> = inner
1468            .adjacency
1469            .iter()
1470            .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
1471            .map(|(id, _)| id.clone())
1472            .collect();
1473        ids.sort_unstable();
1474        Ok(ids)
1475    }
1476
1477    /// Update the label of an existing entity in-place.
1478    ///
1479    /// Returns `Ok(true)` if the entity was found and updated, `Ok(false)` if not found.
1480    pub fn update_entity_label(
1481        &self,
1482        id: &EntityId,
1483        new_label: impl Into<String>,
1484    ) -> Result<bool, AgentRuntimeError> {
1485        let mut inner = recover_lock(self.inner.lock(), "update_entity_label");
1486        if let Some(entity) = inner.entities.get_mut(id) {
1487            entity.label = new_label.into();
1488            inner.cycle_cache = None;
1489            Ok(true)
1490        } else {
1491            Ok(false)
1492        }
1493    }
1494
1495    /// Compute normalized degree centrality for each entity.
1496    /// Degree = (in_degree + out_degree) / (n - 1), or 0.0 if n <= 1.
1497    pub fn degree_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1498        let inner = recover_lock(self.inner.lock(), "degree_centrality");
1499        let n = inner.entities.len();
1500
1501        // Both out-degree and in-degree are now available from the index:
1502        // adjacency[id].len()         == out-degree
1503        // reverse_adjacency[id].len() == in-degree
1504        let denom = if n <= 1 { 1.0 } else { (n - 1) as f32 };
1505        let mut result = HashMap::new();
1506        for id in inner.entities.keys() {
1507            let od = inner.adjacency.get(id).map_or(0, |v| v.len());
1508            let id_ = inner.reverse_adjacency.get(id).map_or(0, |v| v.len());
1509            let centrality = if n <= 1 {
1510                0.0
1511            } else {
1512                (od + id_) as f32 / denom
1513            };
1514            result.insert(id.clone(), centrality);
1515        }
1516
1517        Ok(result)
1518    }
1519
1520    /// Compute normalized betweenness centrality for each entity.
1521    /// Uses Brandes' algorithm with hop-count BFS.
1522    ///
1523    /// # Complexity
1524    /// O(V * E) time. Not suitable for very large graphs (>1000 nodes).
1525    pub fn betweenness_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1526        let inner = recover_lock(self.inner.lock(), "betweenness_centrality");
1527        let n = inner.entities.len();
1528        let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1529
1530        let mut centrality: HashMap<EntityId, f32> =
1531            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1532
1533        // Pre-allocate per-source work buffers and reuse them each iteration
1534        // to avoid O(V) HashMap allocations * V source nodes = O(V²) allocations.
1535        let mut stack: Vec<EntityId> = Vec::with_capacity(n);
1536        let mut predecessors: HashMap<EntityId, Vec<EntityId>> =
1537            nodes.iter().map(|id| (id.clone(), vec![])).collect();
1538        let mut sigma: HashMap<EntityId, f32> =
1539            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1540        let mut dist: HashMap<EntityId, i64> =
1541            nodes.iter().map(|id| (id.clone(), -1i64)).collect();
1542        let mut delta: HashMap<EntityId, f32> =
1543            nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1544        let mut queue: VecDeque<EntityId> = VecDeque::with_capacity(n);
1545
1546        for source in &nodes {
1547            // BFS to find shortest path counts and predecessors.
1548            // Reset per-source state by clearing rather than re-allocating.
1549            stack.clear();
1550            for v in predecessors.values_mut() {
1551                v.clear();
1552            }
1553            for v in sigma.values_mut() {
1554                *v = 0.0;
1555            }
1556            for v in dist.values_mut() {
1557                *v = -1;
1558            }
1559            for v in delta.values_mut() {
1560                *v = 0.0;
1561            }
1562            queue.clear();
1563
1564            *sigma.entry(source.clone()).or_insert(0.0) = 1.0;
1565            *dist.entry(source.clone()).or_insert(-1) = 0;
1566            queue.push_back(source.clone());
1567
1568            while let Some(v) = queue.pop_front() {
1569                stack.push(v.clone());
1570                let d_v = *dist.get(&v).unwrap_or(&0);
1571                let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
1572                // Use adjacency index for O(degree) neighbour lookup.
1573                if let Some(rels) = inner.adjacency.get(&v) {
1574                    for rel in rels {
1575                        let w = &rel.to;
1576                        let d_w = dist.get(w).copied().unwrap_or(-1);
1577                        if d_w < 0 {
1578                            queue.push_back(w.clone());
1579                            *dist.entry(w.clone()).or_insert(-1) = d_v + 1;
1580                        }
1581                        if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
1582                            *sigma.entry(w.clone()).or_insert(0.0) += sigma_v;
1583                            predecessors.entry(w.clone()).or_default().push(v.clone());
1584                        }
1585                    }
1586                }
1587            }
1588
1589            // (delta map reset above at start of loop)
1590
1591            while let Some(w) = stack.pop() {
1592                let delta_w = *delta.get(&w).unwrap_or(&0.0);
1593                let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
1594                // Item 5 — iterate predecessors by reference to avoid cloning the Vec.
1595                for v in predecessors
1596                    .get(&w)
1597                    .map(|ps| ps.as_slice())
1598                    .unwrap_or_default()
1599                {
1600                    let sigma_v = *sigma.get(v).unwrap_or(&1.0);
1601                    let contribution = (sigma_v / sigma_w) * (1.0 + delta_w);
1602                    *delta.entry(v.clone()).or_insert(0.0) += contribution;
1603                }
1604                if &w != source {
1605                    *centrality.entry(w.clone()).or_insert(0.0) += delta_w;
1606                }
1607            }
1608        }
1609
1610        // Normalize by 2 / ((n-1) * (n-2)) for directed graphs.
1611        if n > 2 {
1612            let norm = 2.0 / (((n - 1) * (n - 2)) as f32);
1613            for v in centrality.values_mut() {
1614                *v *= norm;
1615            }
1616        } else {
1617            for v in centrality.values_mut() {
1618                *v = 0.0;
1619            }
1620        }
1621
1622        Ok(centrality)
1623    }
1624
1625    /// Detect communities using label propagation.
1626    /// Each entity starts as its own community. In each iteration, each entity
1627    /// adopts the most frequent label among its neighbours.
1628    /// Returns a map of entity ID → community ID (usize).
1629    pub fn label_propagation_communities(
1630        &self,
1631        max_iterations: usize,
1632    ) -> Result<HashMap<EntityId, usize>, AgentRuntimeError> {
1633        let inner = recover_lock(self.inner.lock(), "label_propagation_communities");
1634        let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1635
1636        // Build a temporary reverse-adjacency (incoming edges) so that each
1637        // node can cheaply query both its out-neighbours (via adjacency) and
1638        // its in-neighbours (via reverse_adj) without an O(|E|) scan per node.
1639        let mut reverse_adj: HashMap<EntityId, Vec<EntityId>> =
1640            nodes.iter().map(|id| (id.clone(), vec![])).collect();
1641        for rel in &inner.relationships {
1642            reverse_adj
1643                .entry(rel.to.clone())
1644                .or_default()
1645                .push(rel.from.clone());
1646        }
1647
1648        // Assign each node a unique initial label (index in nodes vec).
1649        let mut labels: HashMap<EntityId, usize> = nodes
1650            .iter()
1651            .enumerate()
1652            .map(|(i, id)| (id.clone(), i))
1653            .collect();
1654
1655        // Hoist the frequency counter out of the inner loop to avoid
1656        // allocating a fresh HashMap for every node on every iteration.
1657        let mut freq: HashMap<usize, usize> = HashMap::new();
1658
1659        for _ in 0..max_iterations {
1660            let mut changed = false;
1661            // Iterate in a stable order.
1662            for node in &nodes {
1663                // Collect neighbour labels using adjacency (out) + reverse_adj (in).
1664                let out_labels = inner
1665                    .adjacency
1666                    .get(node)
1667                    .map(|rels| {
1668                        rels.iter()
1669                            .map(|r| labels.get(&r.to).copied().unwrap_or(0))
1670                    })
1671                    .into_iter()
1672                    .flatten();
1673                let in_labels = reverse_adj
1674                    .get(node)
1675                    .map(|froms| froms.iter().map(|f| labels.get(f).copied().unwrap_or(0)))
1676                    .into_iter()
1677                    .flatten();
1678
1679                freq.clear();
1680                for lbl in out_labels.chain(in_labels) {
1681                    *freq.entry(lbl).or_insert(0) += 1;
1682                }
1683
1684                if freq.is_empty() {
1685                    continue;
1686                }
1687
1688                // Find the most frequent label.
1689                let best = freq
1690                    .iter()
1691                    .max_by_key(|&(_, count)| count)
1692                    .map(|(&lbl, _)| lbl);
1693
1694                if let Some(new_label) = best {
1695                    let current = labels.entry(node.clone()).or_insert(0);
1696                    if *current != new_label {
1697                        *current = new_label;
1698                        changed = true;
1699                    }
1700                }
1701            }
1702
1703            if !changed {
1704                break;
1705            }
1706        }
1707
1708        Ok(labels)
1709    }
1710
1711    /// Detect whether the directed graph contains any cycles.
1712    ///
1713    /// Uses iterative DFS with a three-color marking scheme.  The result is
1714    /// cached until the next mutation (`add_entity`, `add_relationship`, or
1715    /// `remove_entity`).
1716    ///
1717    /// # Returns
1718    /// - `Ok(true)` — at least one cycle exists
1719    /// - `Ok(false)` — the graph is acyclic (a DAG)
1720    pub fn detect_cycles(&self) -> Result<bool, AgentRuntimeError> {
1721        let mut inner = recover_lock(self.inner.lock(), "detect_cycles");
1722
1723        if let Some(cached) = inner.cycle_cache {
1724            return Ok(cached);
1725        }
1726
1727        // Iterative DFS with three-color marking: 0=white, 1=gray, 2=black.
1728        let mut color: HashMap<&EntityId, u8> =
1729            inner.entities.keys().map(|id| (id, 0u8)).collect();
1730
1731        let has_cycle = 'outer: {
1732            for start in inner.entities.keys() {
1733                if *color.get(start).unwrap_or(&0) != 0 {
1734                    continue;
1735                }
1736
1737                // Stack holds (node_id, iterator position for adjacency).
1738                let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1739                *color.entry(start).or_insert(0) = 1;
1740
1741                while let Some((node, idx)) = stack.last_mut() {
1742                    // Use the adjacency index (O(1) lookup) instead of an
1743                    // O(|E|) relationship scan on every while-loop step.
1744                    let rels = inner
1745                        .adjacency
1746                        .get(*node)
1747                        .map(|v| v.as_slice())
1748                        .unwrap_or(&[]);
1749
1750                    if *idx < rels.len() {
1751                        let next = &rels[*idx].to;
1752                        *idx += 1;
1753                        match color.get(next).copied().unwrap_or(0) {
1754                            1 => break 'outer true, // back edge → cycle
1755                            0 => {
1756                                *color.entry(next).or_insert(0) = 1;
1757                                stack.push((next, 0));
1758                            }
1759                            _ => {} // already fully processed
1760                        }
1761                    } else {
1762                        // All neighbors processed; color black.
1763                        *color.entry(*node).or_insert(0) = 2;
1764                        stack.pop();
1765                    }
1766                }
1767            }
1768            false
1769        };
1770
1771        inner.cycle_cache = Some(has_cycle);
1772        Ok(has_cycle)
1773    }
1774
1775    /// Compute a topological ordering of all entities.
1776    ///
1777    /// Returns the entities sorted so that for every directed edge `(A → B)`,
1778    /// `A` appears before `B` in the result.  Uses iterative DFS (post-order).
1779    ///
1780    /// # Errors
1781    /// Returns `Err(AgentRuntimeError::Graph(...))` if the graph contains a cycle.
1782    pub fn topological_sort(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1783        let inner = recover_lock(self.inner.lock(), "topological_sort");
1784
1785        // Three-color DFS: 0=white, 1=gray (in-stack), 2=black (done).
1786        let mut color: HashMap<&EntityId, u8> =
1787            inner.entities.keys().map(|id| (id, 0u8)).collect();
1788        let mut result: Vec<EntityId> = Vec::with_capacity(inner.entities.len());
1789
1790        for start in inner.entities.keys() {
1791            if *color.get(start).unwrap_or(&0) != 0 {
1792                continue;
1793            }
1794            // Stack: (node, adjacency_index, already_pushed_post_order)
1795            let mut stack: Vec<(&EntityId, usize, bool)> = vec![(start, 0, false)];
1796            *color.entry(start).or_insert(0) = 1;
1797
1798            while let Some((node, idx, pushed)) = stack.last_mut() {
1799                let rels = inner.adjacency.get(*node).map(|v| v.as_slice()).unwrap_or(&[]);
1800                if *idx < rels.len() {
1801                    let next = &rels[*idx].to;
1802                    *idx += 1;
1803                    match color.get(next).copied().unwrap_or(0) {
1804                        1 => return Err(AgentRuntimeError::Graph(
1805                            "topological_sort: graph contains a cycle".into(),
1806                        )),
1807                        0 => {
1808                            *color.entry(next).or_insert(0) = 1;
1809                            stack.push((next, 0, false));
1810                        }
1811                        _ => {} // already finished
1812                    }
1813                } else {
1814                    if !*pushed {
1815                        result.push((*node).clone());
1816                        *pushed = true;
1817                    }
1818                    *color.entry(*node).or_insert(0) = 2;
1819                    stack.pop();
1820                }
1821            }
1822        }
1823
1824        result.reverse();
1825        Ok(result)
1826    }
1827
1828    /// Update a single property on an existing entity.
1829    ///
1830    /// Returns `Ok(true)` if the entity was found and updated, `Ok(false)` otherwise.
1831    pub fn update_entity_property(
1832        &self,
1833        id: &EntityId,
1834        key: impl Into<String>,
1835        value: serde_json::Value,
1836    ) -> Result<bool, AgentRuntimeError> {
1837        let mut inner = recover_lock(self.inner.lock(), "update_entity_property");
1838        if let Some(entity) = inner.entities.get_mut(id) {
1839            entity.properties.insert(key.into(), value);
1840            Ok(true)
1841        } else {
1842            Ok(false)
1843        }
1844    }
1845
1846    /// Return `true` if the graph is a DAG (directed acyclic graph).
1847    ///
1848    /// Equivalent to `!detect_cycles()` but reads more naturally in
1849    /// condition expressions.
1850    pub fn is_dag(&self) -> Result<bool, AgentRuntimeError> {
1851        Ok(!self.detect_cycles()?)
1852    }
1853
1854    /// Count the number of weakly connected components in the graph.
1855    ///
1856    /// Treats all edges as undirected for component assignment.  Isolated
1857    /// nodes (no edges) each count as their own component.  Uses Union-Find.
1858    pub fn connected_components(&self) -> Result<usize, AgentRuntimeError> {
1859        let inner = recover_lock(self.inner.lock(), "connected_components");
1860        let ids: Vec<&EntityId> = inner.entities.keys().collect();
1861        if ids.is_empty() {
1862            return Ok(0);
1863        }
1864
1865        // Map EntityId → index for Union-Find.
1866        let idx: HashMap<&EntityId, usize> =
1867            ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
1868        let mut parent: Vec<usize> = (0..ids.len()).collect();
1869
1870        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1871            if parent[x] != x {
1872                parent[x] = find(parent, parent[x]);
1873            }
1874            parent[x]
1875        }
1876
1877        fn union(parent: &mut Vec<usize>, a: usize, b: usize) {
1878            let ra = find(parent, a);
1879            let rb = find(parent, b);
1880            if ra != rb {
1881                parent[ra] = rb;
1882            }
1883        }
1884
1885        for rel in &inner.relationships {
1886            if let (Some(&a), Some(&b)) = (idx.get(&rel.from), idx.get(&rel.to)) {
1887                union(&mut parent, a, b);
1888            }
1889        }
1890
1891        let components = ids
1892            .iter()
1893            .enumerate()
1894            .filter(|(i, _)| find(&mut parent, *i) == *i)
1895            .count();
1896        Ok(components)
1897    }
1898
1899    /// Return `true` if the graph is weakly connected (i.e. has at most one
1900    /// connected component when all edges are treated as undirected).
1901    ///
1902    /// An empty graph is considered weakly connected by convention.
1903    ///
1904    /// # Errors
1905    /// Propagates any lock-poisoning error from [`connected_components`].
1906    pub fn weakly_connected(&self) -> Result<bool, AgentRuntimeError> {
1907        Ok(self.connected_components()? <= 1)
1908    }
1909
1910    /// Return all entities that have no outgoing edges (out-degree == 0).
1911    ///
1912    /// In a DAG these are the leaf nodes. Useful for identifying terminal
1913    /// states in a workflow or dependency graph.
1914    pub fn sink_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1915        let inner = recover_lock(self.inner.lock(), "sink_nodes");
1916        // Use adjacency index: entities with no entry (or an empty entry) have no outgoing edges.
1917        Ok(inner
1918            .entities
1919            .values()
1920            .filter(|e| {
1921                inner
1922                    .adjacency
1923                    .get(&e.id)
1924                    .map_or(true, |v| v.is_empty())
1925            })
1926            .cloned()
1927            .collect())
1928    }
1929
1930    /// Return all entities that have no incoming edges (in-degree == 0).
1931    ///
1932    /// In a DAG these are the root nodes. Useful for finding entry points in
1933    /// a workflow or dependency graph.
1934    pub fn source_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1935        let inner = recover_lock(self.inner.lock(), "source_nodes");
1936        // Use reverse_adjacency: entities with no entry (or an empty entry) have no incoming edges.
1937        Ok(inner
1938            .entities
1939            .values()
1940            .filter(|e| {
1941                inner
1942                    .reverse_adjacency
1943                    .get(&e.id)
1944                    .map_or(true, |v| v.is_empty())
1945            })
1946            .cloned()
1947            .collect())
1948    }
1949
1950    /// Return all entities that have no edges (neither incoming nor outgoing).
1951    pub fn isolates(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1952        let inner = recover_lock(self.inner.lock(), "isolates");
1953        // An entity is isolated if it has no outgoing edges (adjacency) and no
1954        // incoming edges (reverse_adjacency).
1955        Ok(inner
1956            .entities
1957            .values()
1958            .filter(|e| {
1959                inner
1960                    .adjacency
1961                    .get(&e.id)
1962                    .map_or(true, |v| v.is_empty())
1963                    && inner
1964                        .reverse_adjacency
1965                        .get(&e.id)
1966                        .map_or(true, |v| v.is_empty())
1967            })
1968            .cloned()
1969            .collect())
1970    }
1971
1972    /// Return the number of outgoing edges (out-degree) for `id`.
1973    ///
1974    /// Returns `0` if the entity has no outgoing edges or does not exist.
1975    pub fn out_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1976        let inner = recover_lock(self.inner.lock(), "out_degree");
1977        Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
1978    }
1979
1980    /// Return the number of incoming edges (in-degree) for `id`.
1981    ///
1982    /// Uses the reverse adjacency index for O(in-degree) lookup.
1983    pub fn in_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1984        let inner = recover_lock(self.inner.lock(), "in_degree");
1985        Ok(inner
1986            .reverse_adjacency
1987            .get(id)
1988            .map_or(0, |srcs| srcs.len()))
1989    }
1990
1991    /// Return the total degree (in-degree + out-degree) for entity `id`.
1992    ///
1993    /// Returns `0` for unknown entities.  Self-loops are counted once in each
1994    /// direction (so they contribute `2` to the total degree).
1995    pub fn total_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1996        let out = self.out_degree(id)?;
1997        let r#in = self.in_degree(id)?;
1998        Ok(out + r#in)
1999    }
2000
2001    /// Return the sorted property keys for entity `id`.
2002    ///
2003    /// Returns an empty `Vec` if the entity has no properties or does not exist.
2004    pub fn entity_property_keys(&self, id: &EntityId) -> Result<Vec<String>, AgentRuntimeError> {
2005        let inner = recover_lock(self.inner.lock(), "entity_property_keys");
2006        let entity = match inner.entities.get(id) {
2007            Some(e) => e,
2008            None => return Ok(vec![]),
2009        };
2010        let mut keys: Vec<String> = entity.properties.keys().cloned().collect();
2011        keys.sort_unstable();
2012        Ok(keys)
2013    }
2014
2015    /// Return `true` if there is any path from `from` to `to`.
2016    ///
2017    /// Both nodes must exist or returns `Err`. Uses BFS internally.
2018    /// Returns `Ok(false)` if nodes exist but are not connected.
2019    pub fn path_exists(&self, from: &str, to: &str) -> Result<bool, AgentRuntimeError> {
2020        let from_id = EntityId::new(from);
2021        let to_id = EntityId::new(to);
2022        match self.shortest_path(&from_id, &to_id) {
2023            Ok(Some(_)) => Ok(true),
2024            Ok(None) => Ok(false),
2025            Err(e) => Err(e),
2026        }
2027    }
2028
2029    /// Return the hop-count of the shortest path from `from` to `to`.
2030    ///
2031    /// Returns `Some(hops)` if a path exists, `None` if the nodes are
2032    /// disconnected.  Both node IDs must exist or returns `Err`.
2033    pub fn shortest_path_length(
2034        &self,
2035        from: &EntityId,
2036        to: &EntityId,
2037    ) -> Result<Option<usize>, AgentRuntimeError> {
2038        Ok(self.shortest_path(from, to)?.map(|path| path.len().saturating_sub(1)))
2039    }
2040
2041    /// BFS limited by maximum depth and maximum node count.
2042    ///
2043    /// Returns the subset of nodes visited within those limits (including start).
2044    pub fn bfs_bounded(
2045        &self,
2046        start: &str,
2047        max_depth: usize,
2048        max_nodes: usize,
2049    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
2050        let inner = recover_lock(self.inner.lock(), "bfs_bounded");
2051        let start_id = EntityId::new(start);
2052        if !inner.entities.contains_key(&start_id) {
2053            return Err(AgentRuntimeError::Graph(format!(
2054                "start entity '{start}' not found"
2055            )));
2056        }
2057
2058        let mut visited: std::collections::HashMap<EntityId, usize> = std::collections::HashMap::new();
2059        let mut queue: VecDeque<(EntityId, usize)> = VecDeque::new();
2060        let mut result: Vec<EntityId> = Vec::new();
2061
2062        visited.insert(start_id.clone(), 0);
2063        queue.push_back((start_id.clone(), 0));
2064        result.push(start_id);
2065
2066        while let Some((current, depth)) = queue.pop_front() {
2067            if result.len() >= max_nodes {
2068                break;
2069            }
2070            if depth >= max_depth {
2071                continue;
2072            }
2073            for neighbour in Self::neighbours(&inner.adjacency, &current) {
2074                if !visited.contains_key(&neighbour) {
2075                    let new_depth = depth + 1;
2076                    visited.insert(neighbour.clone(), new_depth);
2077                    result.push(neighbour.clone());
2078                    if result.len() >= max_nodes {
2079                        break;
2080                    }
2081                    queue.push_back((neighbour, new_depth));
2082                }
2083            }
2084        }
2085
2086        Ok(result)
2087    }
2088
2089    /// DFS limited by maximum depth and maximum node count.
2090    ///
2091    /// Returns the subset of nodes visited within those limits (including start).
2092    pub fn dfs_bounded(
2093        &self,
2094        start: &str,
2095        max_depth: usize,
2096        max_nodes: usize,
2097    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
2098        let inner = recover_lock(self.inner.lock(), "dfs_bounded");
2099        let start_id = EntityId::new(start);
2100        if !inner.entities.contains_key(&start_id) {
2101            return Err(AgentRuntimeError::Graph(format!(
2102                "start entity '{start}' not found"
2103            )));
2104        }
2105
2106        let mut visited: HashSet<EntityId> = HashSet::new();
2107        let mut stack: Vec<(EntityId, usize)> = Vec::new();
2108        let mut result: Vec<EntityId> = Vec::new();
2109
2110        visited.insert(start_id.clone());
2111        stack.push((start_id.clone(), 0));
2112        result.push(start_id);
2113
2114        while let Some((current, depth)) = stack.pop() {
2115            if result.len() >= max_nodes {
2116                break;
2117            }
2118            if depth >= max_depth {
2119                continue;
2120            }
2121            for neighbour in Self::neighbours(&inner.adjacency, &current) {
2122                if visited.insert(neighbour.clone()) {
2123                    result.push(neighbour.clone());
2124                    if result.len() >= max_nodes {
2125                        break;
2126                    }
2127                    stack.push((neighbour, depth + 1));
2128                }
2129            }
2130        }
2131
2132        Ok(result)
2133    }
2134
2135    /// Return all entities in the graph.
2136    pub fn all_entities(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2137        let inner = recover_lock(self.inner.lock(), "all_entities");
2138        Ok(inner.entities.values().cloned().collect())
2139    }
2140
2141    /// Return all relationships in the graph.
2142    pub fn all_relationships(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
2143        let inner = recover_lock(self.inner.lock(), "all_relationships");
2144        Ok(inner.relationships.clone())
2145    }
2146
2147    /// Return all relationships whose `kind` matches `kind` (case-sensitive).
2148    pub fn find_relationships_by_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
2149        let inner = recover_lock(self.inner.lock(), "find_relationships_by_kind");
2150        Ok(inner.relationships.iter().filter(|r| r.kind == kind).cloned().collect())
2151    }
2152
2153    /// Return the number of relationships whose `kind` matches `kind` (case-sensitive).
2154    pub fn count_relationships_by_kind(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
2155        let inner = recover_lock(self.inner.lock(), "count_relationships_by_kind");
2156        Ok(inner.relationships.iter().filter(|r| r.kind == kind).count())
2157    }
2158
2159    /// Return all entities whose `label` matches `label` (case-sensitive).
2160    pub fn find_entities_by_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2161        let inner = recover_lock(self.inner.lock(), "find_entities_by_label");
2162        Ok(inner
2163            .entities
2164            .values()
2165            .filter(|e| e.label == label)
2166            .cloned()
2167            .collect())
2168    }
2169
2170    /// Return all entities whose label is contained in `labels`.
2171    ///
2172    /// Order of results is unspecified.  An empty `labels` slice returns an
2173    /// empty result (never all entities).
2174    pub fn find_entities_by_labels(&self, labels: &[&str]) -> Result<Vec<Entity>, AgentRuntimeError> {
2175        let inner = recover_lock(self.inner.lock(), "find_entities_by_labels");
2176        let label_set: std::collections::HashSet<&str> = labels.iter().copied().collect();
2177        Ok(inner
2178            .entities
2179            .values()
2180            .filter(|e| label_set.contains(e.label.as_str()))
2181            .cloned()
2182            .collect())
2183    }
2184
2185    /// Remove all isolated entities (those with no incoming or outgoing edges).
2186    ///
2187    /// Returns the number of entities removed.
2188    pub fn remove_isolated(&self) -> Result<usize, AgentRuntimeError> {
2189        let mut inner = recover_lock(self.inner.lock(), "remove_isolated");
2190        let isolated: Vec<EntityId> = inner
2191            .entities
2192            .keys()
2193            .filter(|id| {
2194                inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
2195                    && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
2196            })
2197            .cloned()
2198            .collect();
2199        let count = isolated.len();
2200        for id in &isolated {
2201            inner.entities.remove(id);
2202            inner.adjacency.remove(id);
2203            inner.reverse_adjacency.remove(id);
2204        }
2205        if count > 0 {
2206            inner.cycle_cache = None;
2207        }
2208        Ok(count)
2209    }
2210
2211    /// Return all outgoing relationships from `id` (those where `id` is the source).
2212    pub fn get_relationships_for(
2213        &self,
2214        id: &EntityId,
2215    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2216        let inner = recover_lock(self.inner.lock(), "get_relationships_for");
2217        Ok(inner
2218            .adjacency
2219            .get(id)
2220            .cloned()
2221            .unwrap_or_default())
2222    }
2223
2224    /// Return all relationships where both endpoints are `from` and `to` (any direction or kind).
2225    pub fn relationships_between(
2226        &self,
2227        from: &EntityId,
2228        to: &EntityId,
2229    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2230        let inner = recover_lock(self.inner.lock(), "relationships_between");
2231        Ok(inner
2232            .relationships
2233            .iter()
2234            .filter(|r| {
2235                (r.from == *from && r.to == *to) || (r.from == *to && r.to == *from)
2236            })
2237            .cloned()
2238            .collect())
2239    }
2240
2241    /// Find entities that have a property `key` whose value equals `expected`.
2242    ///
2243    /// Uses `serde_json::Value` equality; for string properties pass
2244    /// `serde_json::Value::String(...)`.
2245    pub fn find_entities_by_property(
2246        &self,
2247        key: &str,
2248        expected: &serde_json::Value,
2249    ) -> Result<Vec<Entity>, AgentRuntimeError> {
2250        let inner = recover_lock(self.inner.lock(), "find_entities_by_property");
2251        Ok(inner
2252            .entities
2253            .values()
2254            .filter(|e| e.properties.get(key) == Some(expected))
2255            .cloned()
2256            .collect())
2257    }
2258
2259    /// Merge another `GraphStore` into this one.
2260    ///
2261    /// Entities are inserted (or replaced if the same ID exists); relationships
2262    /// are inserted only if the `(from, to, kind)` triple does not already exist.
2263    pub fn merge(&self, other: &GraphStore) -> Result<(), AgentRuntimeError> {
2264        let other_inner = recover_lock(other.inner.lock(), "merge:read");
2265        let other_entities: Vec<Entity> = other_inner.entities.values().cloned().collect();
2266        let other_rels: Vec<Relationship> = other_inner.relationships.clone();
2267        drop(other_inner);
2268
2269        let mut inner = recover_lock(self.inner.lock(), "merge:write");
2270        inner.cycle_cache = None;
2271        for entity in other_entities {
2272            inner.adjacency.entry(entity.id.clone()).or_default();
2273            inner.entities.insert(entity.id.clone(), entity);
2274        }
2275        for rel in other_rels {
2276            let already_exists = inner
2277                .relationships
2278                .iter()
2279                .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
2280            if !already_exists && inner.entities.contains_key(&rel.from) && inner.entities.contains_key(&rel.to) {
2281                inner
2282                    .adjacency
2283                    .entry(rel.from.clone())
2284                    .or_default()
2285                    .push(rel.clone());
2286                inner.relationships.push(rel);
2287            }
2288        }
2289        Ok(())
2290    }
2291
2292    /// Return the `Entity` objects for all direct out-neighbors of `id`.
2293    ///
2294    /// Neighbors are the targets of all outgoing relationships from `id`.
2295    /// Entities that no longer exist (orphan edge targets) are silently skipped.
2296    pub fn neighbor_entities(&self, id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
2297        let inner = recover_lock(self.inner.lock(), "neighbor_entities");
2298        let neighbors: Vec<Entity> = inner
2299            .adjacency
2300            .get(id)
2301            .iter()
2302            .flat_map(|rels| rels.iter())
2303            .filter_map(|r| inner.entities.get(&r.to).cloned())
2304            .collect();
2305        Ok(neighbors)
2306    }
2307
2308    /// Return the IDs of all directly reachable neighbours from `id`
2309    /// (i.e. the `to` end of every outgoing relationship).
2310    ///
2311    /// Cheaper than [`neighbor_entities`] when only IDs are needed.
2312    ///
2313    /// [`neighbor_entities`]: GraphStore::neighbor_entities
2314    pub fn neighbor_ids(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2315        let inner = recover_lock(self.inner.lock(), "neighbor_ids");
2316        let ids: Vec<EntityId> = inner
2317            .adjacency
2318            .get(id)
2319            .iter()
2320            .flat_map(|rels| rels.iter())
2321            .map(|r| r.to.clone())
2322            .collect();
2323        Ok(ids)
2324    }
2325
2326    /// Remove **all** relationships where `id` is the source (outgoing edges).
2327    ///
2328    /// Also updates the adjacency index.  Does **not** remove incoming edges
2329    /// from other nodes that point to `id` — use this before [`remove_entity`]
2330    /// to ensure consistent graph state.
2331    ///
2332    /// Returns the number of relationships removed.
2333    ///
2334    /// [`remove_entity`]: GraphStore::remove_entity
2335    pub fn remove_all_relationships_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
2336        let mut inner = recover_lock(self.inner.lock(), "remove_all_relationships_for");
2337        inner.adjacency.remove(id);
2338        let before = inner.relationships.len();
2339        inner.relationships.retain(|r| &r.from != id);
2340        inner.cycle_cache = None;
2341        Ok(before - inner.relationships.len())
2342    }
2343
2344    /// Check whether an entity with the given ID exists.
2345    pub fn entity_exists(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
2346        let inner = recover_lock(self.inner.lock(), "entity_exists");
2347        Ok(inner.entities.contains_key(id))
2348    }
2349
2350    /// Check whether a relationship `(from, to, kind)` exists.
2351    ///
2352    /// Uses the adjacency index (O(out-degree of `from`)) rather than a full
2353    /// O(|E|) scan over all relationships.
2354    pub fn relationship_exists(
2355        &self,
2356        from: &EntityId,
2357        to: &EntityId,
2358        kind: &str,
2359    ) -> Result<bool, AgentRuntimeError> {
2360        let inner = recover_lock(self.inner.lock(), "relationship_exists");
2361        Ok(inner
2362            .adjacency
2363            .get(from)
2364            .map_or(false, |rels| rels.iter().any(|r| r.to == *to && r.kind == kind)))
2365    }
2366
2367    /// Extract a subgraph containing only the specified entities and the
2368    /// relationships between them.
2369    pub fn subgraph(&self, node_ids: &[EntityId]) -> Result<GraphStore, AgentRuntimeError> {
2370        let inner = recover_lock(self.inner.lock(), "subgraph");
2371        let id_set: HashSet<&EntityId> = node_ids.iter().collect();
2372
2373        let new_store = GraphStore::new();
2374
2375        // Validate all entities exist before mutating the new store, so we
2376        // don't end up with a partially-populated subgraph on error.
2377        let entities_to_copy: Vec<Entity> = node_ids
2378            .iter()
2379            .map(|id| {
2380                inner
2381                    .entities
2382                    .get(id)
2383                    .cloned()
2384                    .ok_or_else(|| {
2385                        AgentRuntimeError::Graph(format!("entity '{}' not found", id.0))
2386                    })
2387            })
2388            .collect::<Result<_, _>>()?;
2389
2390        // Acquire the new store's lock once for all entity insertions.
2391        {
2392            let mut new_inner = recover_lock(new_store.inner.lock(), "subgraph:add_entities");
2393            for entity in entities_to_copy {
2394                // Ensure an adjacency entry exists for this entity.
2395                new_inner.adjacency.entry(entity.id.clone()).or_default();
2396                new_inner.entities.insert(entity.id.clone(), entity);
2397            }
2398        }
2399
2400        // Acquire the new store's lock once for the entire relationship batch
2401        // rather than once per relationship.
2402        {
2403            let mut new_inner =
2404                recover_lock(new_store.inner.lock(), "subgraph:add_relationships");
2405            for rel in inner.relationships.iter() {
2406                if id_set.contains(&rel.from) && id_set.contains(&rel.to) {
2407                    // Keep adjacency index in sync with relationships.
2408                    new_inner
2409                        .adjacency
2410                        .entry(rel.from.clone())
2411                        .or_default()
2412                        .push(rel.clone());
2413                    new_inner.relationships.push(rel.clone());
2414                }
2415            }
2416        }
2417
2418        Ok(new_store)
2419    }
2420
2421    /// Return a new graph with all edge directions reversed.
2422    ///
2423    /// Every relationship `A → B` becomes `B → A` in the returned graph.
2424    /// All entities are copied; relationship weights and kinds are preserved.
2425    pub fn reverse(&self) -> Result<GraphStore, AgentRuntimeError> {
2426        let inner = recover_lock(self.inner.lock(), "reverse");
2427        let reversed = GraphStore::new();
2428
2429        // Copy all entities.
2430        {
2431            let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:entities");
2432            for entity in inner.entities.values() {
2433                r_inner.adjacency.entry(entity.id.clone()).or_default();
2434                r_inner.entities.insert(entity.id.clone(), entity.clone());
2435            }
2436        }
2437
2438        // Add reversed relationships.
2439        for rel in &inner.relationships {
2440            let flipped = Relationship {
2441                from: rel.to.clone(),
2442                to: rel.from.clone(),
2443                kind: rel.kind.clone(),
2444                weight: rel.weight,
2445            };
2446            let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:rels");
2447            r_inner
2448                .adjacency
2449                .entry(flipped.from.clone())
2450                .or_default()
2451                .push(flipped.clone());
2452            r_inner.relationships.push(flipped);
2453        }
2454
2455        Ok(reversed)
2456    }
2457
2458    /// Return entities that are out-neighbors of **both** `a` and `b`.
2459    ///
2460    /// Useful for link-prediction and community detection heuristics.
2461    /// Returns an empty `Vec` if either node has no outgoing edges.
2462    pub fn common_neighbors(
2463        &self,
2464        a: &EntityId,
2465        b: &EntityId,
2466    ) -> Result<Vec<Entity>, AgentRuntimeError> {
2467        let inner = recover_lock(self.inner.lock(), "common_neighbors");
2468        let a_set: HashSet<&EntityId> = inner
2469            .adjacency
2470            .get(a)
2471            .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2472        let b_set: HashSet<&EntityId> = inner
2473            .adjacency
2474            .get(b)
2475            .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2476        let common: Vec<Entity> = a_set
2477            .intersection(&b_set)
2478            .filter_map(|id| inner.entities.get(*id).cloned())
2479            .collect();
2480        Ok(common)
2481    }
2482
2483    /// Return the weight of the first relationship from `from` to `to`.
2484    ///
2485    /// Returns `None` if no such edge exists.
2486    pub fn weight_of(
2487        &self,
2488        from: &EntityId,
2489        to: &EntityId,
2490    ) -> Result<Option<f32>, AgentRuntimeError> {
2491        let inner = recover_lock(self.inner.lock(), "weight_of");
2492        let weight = inner
2493            .adjacency
2494            .get(from)
2495            .and_then(|rels| rels.iter().find(|r| &r.to == to))
2496            .map(|r| r.weight);
2497        Ok(weight)
2498    }
2499
2500    /// Return the IDs of all entities that have an outgoing edge pointing **to** `id`.
2501    ///
2502    /// This is the inverse of `neighbor_entities`, which returns out-neighbors.
2503    /// Returns an empty `Vec` if no incoming edges exist for `id`.
2504    pub fn neighbors_in(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2505        let inner = recover_lock(self.inner.lock(), "neighbors_in");
2506        // Use reverse_adjacency for O(in-degree) lookup instead of O(|E|) scan.
2507        Ok(inner
2508            .reverse_adjacency
2509            .get(id)
2510            .cloned()
2511            .unwrap_or_default())
2512    }
2513
2514    /// Return the graph density: `edges / (nodes * (nodes − 1))` for a directed graph.
2515    ///
2516    /// Returns `0.0` when the graph has fewer than 2 nodes (no directed edges are
2517    /// possible). Values range from `0.0` (sparse) to `1.0` (complete).
2518    pub fn density(&self) -> Result<f64, AgentRuntimeError> {
2519        let inner = recover_lock(self.inner.lock(), "density");
2520        let n = inner.entities.len();
2521        if n < 2 {
2522            return Ok(0.0);
2523        }
2524        let max_edges = n * (n - 1);
2525        Ok(inner.relationships.len() as f64 / max_edges as f64)
2526    }
2527
2528    /// Return the average out-degree across all nodes.
2529    ///
2530    /// Returns `0.0` for an empty graph.
2531    pub fn avg_degree(&self) -> Result<f64, AgentRuntimeError> {
2532        let inner = recover_lock(self.inner.lock(), "avg_degree");
2533        let n = inner.entities.len();
2534        if n == 0 {
2535            return Ok(0.0);
2536        }
2537        Ok(inner.relationships.len() as f64 / n as f64)
2538    }
2539
2540    /// Return the sum of all edge weights in the graph.
2541    ///
2542    /// Returns `0.0` for an empty graph or a graph with no relationships.
2543    pub fn total_weight(&self) -> Result<f32, AgentRuntimeError> {
2544        let inner = recover_lock(self.inner.lock(), "total_weight");
2545        Ok(inner.relationships.iter().map(|r| r.weight).sum())
2546    }
2547
2548    /// Return the maximum edge weight, or `None` if the graph has no edges.
2549    pub fn max_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2550        let inner = recover_lock(self.inner.lock(), "max_edge_weight");
2551        Ok(inner
2552            .relationships
2553            .iter()
2554            .map(|r| r.weight)
2555            .reduce(f32::max))
2556    }
2557
2558    /// Return the minimum edge weight, or `None` if the graph has no edges.
2559    pub fn min_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2560        let inner = recover_lock(self.inner.lock(), "min_edge_weight");
2561        Ok(inner
2562            .relationships
2563            .iter()
2564            .map(|r| r.weight)
2565            .reduce(f32::min))
2566    }
2567
2568    /// Return the top-N entities by out-degree (most outgoing edges first).
2569    ///
2570    /// If `n == 0` or the graph is empty, returns an empty Vec.  Ties are
2571    /// broken by arbitrary hash-map iteration order.
2572    pub fn top_n_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2573        if n == 0 {
2574            return Ok(Vec::new());
2575        }
2576        let inner = recover_lock(self.inner.lock(), "top_n_by_out_degree");
2577        let mut pairs: Vec<(&EntityId, usize)> = inner
2578            .adjacency
2579            .iter()
2580            .map(|(id, rels)| (id, rels.len()))
2581            .collect();
2582        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2583        Ok(pairs
2584            .into_iter()
2585            .take(n)
2586            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2587            .collect())
2588    }
2589
2590    /// Remove `id` and all relationships where it is the source or target.
2591    ///
2592    /// Equivalent to calling `remove_entity` and `remove_all_relationships_for`
2593    /// in a single lock acquisition, avoiding two separate look-ups.
2594    ///
2595    /// Returns `Ok(())` if the entity was found and removed.  Returns
2596    /// `Err(AgentRuntimeError::Graph)` if no entity with that ID exists.
2597    pub fn remove_entity_and_edges(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
2598        let mut inner = recover_lock(self.inner.lock(), "remove_entity_and_edges");
2599        if !inner.entities.contains_key(id) {
2600            return Err(AgentRuntimeError::Graph(format!(
2601                "entity '{}' not found",
2602                id.0
2603            )));
2604        }
2605        inner.entities.remove(id);
2606        inner.relationships.retain(|r| &r.from != id && &r.to != id);
2607        inner.adjacency.remove(id);
2608        for adj in inner.adjacency.values_mut() {
2609            adj.retain(|r| &r.to != id);
2610        }
2611        inner.reverse_adjacency.remove(id);
2612        for rev in inner.reverse_adjacency.values_mut() {
2613            rev.retain(|src| src != id);
2614        }
2615        inner.cycle_cache = None;
2616        Ok(())
2617    }
2618
2619    /// Return all entities whose out-degree is at or above `threshold`.
2620    ///
2621    /// Useful for finding hub or gateway nodes in a knowledge graph.
2622    pub fn hub_nodes(&self, threshold: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2623        let inner = recover_lock(self.inner.lock(), "hub_nodes");
2624        Ok(inner
2625            .entities
2626            .values()
2627            .filter(|e| {
2628                inner
2629                    .adjacency
2630                    .get(&e.id)
2631                    .map_or(0, |rels| rels.len())
2632                    >= threshold
2633            })
2634            .cloned()
2635            .collect())
2636    }
2637
2638    /// Return all relationships incident to `entity_id`
2639    /// (where the entity is either the source or the target).
2640    pub fn incident_relationships(
2641        &self,
2642        entity_id: &EntityId,
2643    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2644        let inner = recover_lock(self.inner.lock(), "incident_relationships");
2645        Ok(inner
2646            .relationships
2647            .iter()
2648            .filter(|r| &r.from == entity_id || &r.to == entity_id)
2649            .cloned()
2650            .collect())
2651    }
2652
2653    /// Return the entity with the highest out-degree (most outgoing edges),
2654    /// or `None` if the graph is empty.
2655    ///
2656    /// If multiple entities share the maximum out-degree, the first one
2657    /// encountered (in arbitrary hash-map iteration order) is returned.
2658    pub fn max_out_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2659        let inner = recover_lock(self.inner.lock(), "max_out_degree_entity");
2660        let best = inner
2661            .adjacency
2662            .iter()
2663            .max_by_key(|(_, rels)| rels.len())
2664            .and_then(|(id, _)| inner.entities.get(id).cloned());
2665        Ok(best)
2666    }
2667
2668    /// Return the entity with the most incoming edges (highest in-degree).
2669    ///
2670    /// Uses the reverse adjacency index for O(V) computation.
2671    /// Returns `None` for an empty graph or a graph with no edges.
2672    pub fn max_in_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2673        let inner = recover_lock(self.inner.lock(), "max_in_degree_entity");
2674        let best = inner
2675            .reverse_adjacency
2676            .iter()
2677            .max_by_key(|(_, srcs)| srcs.len())
2678            .and_then(|(id, _)| inner.entities.get(id).cloned());
2679        Ok(best)
2680    }
2681
2682    /// Return all entities with out-degree = 0 (no outgoing edges).
2683    ///
2684    /// Leaf nodes (also called sink nodes in directed graphs) are entities
2685    /// that have no outgoing relationships.  See also [`sink_nodes`] which
2686    /// also counts nodes with no outgoing edges but filters differently.
2687    ///
2688    /// [`sink_nodes`]: GraphStore::sink_nodes
2689    pub fn leaf_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2690        let inner = recover_lock(self.inner.lock(), "leaf_nodes");
2691        Ok(inner
2692            .entities
2693            .values()
2694            .filter(|e| {
2695                inner
2696                    .adjacency
2697                    .get(&e.id)
2698                    .map_or(true, |rels| rels.is_empty())
2699            })
2700            .cloned()
2701            .collect())
2702    }
2703
2704    /// Return the top `n` entities sorted by out-degree (descending).
2705    ///
2706    /// Uses the adjacency index for O(V log V) computation.  Useful for finding
2707    /// hub nodes — entities with the most outgoing connections.
2708    /// Returns fewer than `n` entities if the graph has fewer nodes.
2709    pub fn top_nodes_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2710        let inner = recover_lock(self.inner.lock(), "top_nodes_by_out_degree");
2711        let mut pairs: Vec<(&EntityId, usize)> = inner
2712            .entities
2713            .keys()
2714            .map(|id| (id, inner.adjacency.get(id).map_or(0, |v| v.len())))
2715            .collect();
2716        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2717        pairs.truncate(n);
2718        Ok(pairs
2719            .into_iter()
2720            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2721            .collect())
2722    }
2723
2724    /// Return the top `n` entities sorted by in-degree (descending).
2725    ///
2726    /// Uses the reverse adjacency index for O(V log V) computation.  Useful for
2727    /// finding sink hubs — entities with the most incoming connections.
2728    pub fn top_nodes_by_in_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2729        let inner = recover_lock(self.inner.lock(), "top_nodes_by_in_degree");
2730        let mut pairs: Vec<(&EntityId, usize)> = inner
2731            .entities
2732            .keys()
2733            .map(|id| (id, inner.reverse_adjacency.get(id).map_or(0, |v| v.len())))
2734            .collect();
2735        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2736        pairs.truncate(n);
2737        Ok(pairs
2738            .into_iter()
2739            .filter_map(|(id, _)| inner.entities.get(id).cloned())
2740            .collect())
2741    }
2742
2743    /// Return a map of relationship type label → count across the entire graph.
2744    ///
2745    /// Useful for understanding the composition of a knowledge graph at a glance.
2746    /// Returns an empty map when the graph has no relationships.
2747    pub fn relationship_type_counts(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
2748        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_counts");
2749        let mut counts: HashMap<String, usize> = HashMap::new();
2750        for rel in &inner.relationships {
2751            *counts.entry(rel.kind.clone()).or_insert(0) += 1;
2752        }
2753        Ok(counts)
2754    }
2755
2756    /// Return all entities that do **not** have a property with `key`.
2757    ///
2758    /// Useful for finding incomplete nodes that need a required attribute filled
2759    /// in before they can participate in downstream graph queries.
2760    pub fn entities_without_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2761        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_property");
2762        Ok(inner
2763            .entities
2764            .values()
2765            .filter(|e| !e.properties.contains_key(key))
2766            .cloned()
2767            .collect())
2768    }
2769
2770    /// Return a sorted, deduplicated list of all relationship type labels
2771    /// present in the graph.
2772    ///
2773    /// Returns an empty `Vec` when the graph has no relationships.
2774    pub fn unique_relationship_types(&self) -> Result<Vec<String>, AgentRuntimeError> {
2775        let inner = recover_lock(self.inner.lock(), "GraphStore::unique_relationship_types");
2776        let mut kinds: Vec<String> = inner
2777            .relationships
2778            .iter()
2779            .map(|r| r.kind.clone())
2780            .collect::<std::collections::HashSet<_>>()
2781            .into_iter()
2782            .collect();
2783        kinds.sort_unstable();
2784        Ok(kinds)
2785    }
2786
2787    /// Return the average number of properties per entity.
2788    ///
2789    /// Returns `0.0` when the graph is empty.
2790    pub fn avg_property_count(&self) -> Result<f64, AgentRuntimeError> {
2791        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_property_count");
2792        let n = inner.entities.len();
2793        if n == 0 {
2794            return Ok(0.0);
2795        }
2796        let total: usize = inner.entities.values().map(|e| e.properties.len()).sum();
2797        Ok(total as f64 / n as f64)
2798    }
2799
2800    /// Return a map of property key → number of entities that have that key.
2801    ///
2802    /// Useful for auditing schema coverage: a key that appears on all entities
2803    /// has count == entity_count; a key that appears on only one entity may
2804    /// indicate a one-off annotation.
2805    ///
2806    /// Returns an empty map when the graph has no entities or no entity has
2807    /// any properties.
2808    pub fn property_key_frequency(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
2809        let inner = recover_lock(self.inner.lock(), "GraphStore::property_key_frequency");
2810        let mut freq: HashMap<String, usize> = HashMap::new();
2811        for entity in inner.entities.values() {
2812            for key in entity.properties.keys() {
2813                *freq.entry(key.clone()).or_insert(0) += 1;
2814            }
2815        }
2816        Ok(freq)
2817    }
2818
2819    /// Return all entities sorted ascending by their label string.
2820    ///
2821    /// Entities with the same label are ordered by their ID for a stable, fully
2822    /// deterministic output regardless of internal hash-map iteration order.
2823    pub fn entities_sorted_by_label(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2824        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_label");
2825        let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
2826        entities.sort_unstable_by(|a, b| a.label.cmp(&b.label).then_with(|| a.id.cmp(&b.id)));
2827        Ok(entities)
2828    }
2829
2830    /// Return all entities that have the given property `key`, regardless of value.
2831    ///
2832    /// Returns an empty `Vec` when no entities carry the key or the graph is empty.
2833    pub fn entities_with_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2834        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property");
2835        let entities: Vec<Entity> = inner
2836            .entities
2837            .values()
2838            .filter(|e| e.properties.contains_key(key))
2839            .cloned()
2840            .collect();
2841        Ok(entities)
2842    }
2843
2844    /// Return the total number of directed edges stored in this graph.
2845    ///
2846    /// Each relationship contributes exactly one edge to this count, so a
2847    /// bidirectional pair of relationships contributes two.
2848    pub fn total_relationship_count(&self) -> Result<usize, AgentRuntimeError> {
2849        let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_count");
2850        Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
2851    }
2852
2853    /// Format a slice of `EntityId`s as a human-readable path string.
2854    ///
2855    /// Each ID is rendered using its label if the entity exists in the store,
2856    /// otherwise the raw ID string is used.  Nodes are joined with ` → `.
2857    ///
2858    /// Returns an empty `String` for an empty `path` slice.
2859    ///
2860    /// # Example
2861    /// ```text
2862    /// "Alice → Bob → Carol"
2863    /// ```
2864    pub fn path_to_string(&self, path: &[EntityId]) -> Result<String, AgentRuntimeError> {
2865        let inner = recover_lock(self.inner.lock(), "GraphStore::path_to_string");
2866        let parts: Vec<String> = path
2867            .iter()
2868            .map(|id| {
2869                inner
2870                    .entities
2871                    .get(id)
2872                    .map(|e| e.label.clone())
2873                    .unwrap_or_else(|| id.0.clone())
2874            })
2875            .collect();
2876        Ok(parts.join(" \u{2192} "))
2877    }
2878
2879    /// Return all relationships whose `kind` equals `kind` (case-sensitive).
2880    ///
2881    /// Returns an empty `Vec` when no relationships of that kind exist.
2882    pub fn relationships_of_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
2883        let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_of_kind");
2884        let mut result = Vec::new();
2885        for rels in inner.adjacency.values() {
2886            for rel in rels {
2887                if rel.kind == kind {
2888                    result.push(rel.clone());
2889                }
2890            }
2891        }
2892        Ok(result)
2893    }
2894
2895    /// Return all entities whose `label` does **not** equal `label`.
2896    ///
2897    /// Useful for filtering out a specific entity type without loading all
2898    /// entities first.  Returns all entities for an empty graph.
2899    pub fn entities_without_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2900        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_label");
2901        Ok(inner
2902            .entities
2903            .values()
2904            .filter(|e| e.label != label)
2905            .cloned()
2906            .collect())
2907    }
2908
2909    /// Return all entities that have no outgoing edges (out-degree == 0).
2910    ///
2911    /// Entities that appear only as relationship targets, or entities with no
2912    /// relationships at all, are included.
2913    pub fn entities_without_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2914        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_outgoing");
2915        let entities: Vec<Entity> = inner
2916            .entities
2917            .values()
2918            .filter(|e| !inner.adjacency.contains_key(&e.id) || inner.adjacency[&e.id].is_empty())
2919            .cloned()
2920            .collect();
2921        Ok(entities)
2922    }
2923
2924    /// Return all entities that have no incoming edges (in-degree == 0).
2925    ///
2926    /// These are entities that no other entity points to — i.e., root / source
2927    /// nodes in the directed graph.
2928    pub fn entities_without_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2929        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_incoming");
2930        let entities: Vec<Entity> = inner
2931            .entities
2932            .values()
2933            .filter(|e| !inner.reverse_adjacency.contains_key(&e.id) || inner.reverse_adjacency[&e.id].is_empty())
2934            .cloned()
2935            .collect();
2936        Ok(entities)
2937    }
2938
2939    /// Return the total out-degree of the graph — the sum of the number of
2940    /// outgoing edges across all entities.
2941    ///
2942    /// Equivalent to `total_relationship_count` but traverses the adjacency
2943    /// list directly without accessing the reverse index.
2944    pub fn total_out_degree(&self) -> Result<usize, AgentRuntimeError> {
2945        let inner = recover_lock(self.inner.lock(), "GraphStore::total_out_degree");
2946        Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
2947    }
2948
2949    /// Return all relationships whose `weight` is strictly greater than
2950    /// `threshold`.
2951    ///
2952    /// Returns an empty `Vec` when the graph has no relationships or none
2953    /// exceeds the threshold.
2954    pub fn relationships_with_weight_above(
2955        &self,
2956        threshold: f32,
2957    ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2958        let inner = recover_lock(
2959            self.inner.lock(),
2960            "GraphStore::relationships_with_weight_above",
2961        );
2962        let rels: Vec<Relationship> = inner
2963            .adjacency
2964            .values()
2965            .flat_map(|rels| rels.iter())
2966            .filter(|r| r.weight > threshold)
2967            .cloned()
2968            .collect();
2969        Ok(rels)
2970    }
2971
2972    /// Return the entity with the most properties, or `None` for an empty graph.
2973    ///
2974    /// When multiple entities share the maximum property count the one whose
2975    /// ID sorts first lexicographically is returned for deterministic output.
2976    pub fn entity_with_most_properties(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2977        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_most_properties");
2978        let entity = inner.entities.values().max_by(|a, b| {
2979            a.properties
2980                .len()
2981                .cmp(&b.properties.len())
2982                .then_with(|| b.id.0.cmp(&a.id.0))
2983        });
2984        Ok(entity.cloned())
2985    }
2986
2987    /// Return the arithmetic mean weight across all relationships, or `None`
2988    /// if the graph has no relationships.
2989    pub fn avg_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
2990        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight");
2991        let all: Vec<f32> = inner
2992            .adjacency
2993            .values()
2994            .flat_map(|rels| rels.iter().map(|r| r.weight))
2995            .collect();
2996        if all.is_empty() {
2997            return Ok(None);
2998        }
2999        let sum: f64 = all.iter().map(|&w| w as f64).sum();
3000        Ok(Some(sum / all.len() as f64))
3001    }
3002
3003    /// Return the number of entities whose `label` matches `label` exactly.
3004    ///
3005    /// Returns `0` when no entities carry that label.
3006    pub fn entity_count_with_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
3007        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_count_with_label");
3008        Ok(inner.entities.values().filter(|e| e.label == label).count())
3009    }
3010
3011    /// Return the count of directed edges whose weight is strictly above `threshold`.
3012    ///
3013    /// Unlike [`relationships_with_weight_above`] this performs a lightweight
3014    /// count-only scan without cloning relationship data.
3015    ///
3016    /// [`relationships_with_weight_above`]: GraphStore::relationships_with_weight_above
3017    pub fn edge_count_above_weight(&self, threshold: f32) -> Result<usize, AgentRuntimeError> {
3018        let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_above_weight");
3019        Ok(inner
3020            .adjacency
3021            .values()
3022            .flat_map(|rels| rels.iter())
3023            .filter(|r| r.weight > threshold)
3024            .count())
3025    }
3026
3027    /// Return all entities whose label starts with `prefix`.
3028    ///
3029    /// Returns an empty `Vec` when no entities match or the graph is empty.
3030    pub fn entities_with_label_prefix(&self, prefix: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3031        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_prefix");
3032        let entities: Vec<Entity> = inner
3033            .entities
3034            .values()
3035            .filter(|e| e.label.starts_with(prefix))
3036            .cloned()
3037            .collect();
3038        Ok(entities)
3039    }
3040
3041    /// Return entity-ID pairs `(a, b)` where both `a → b` and `b → a` edges exist.
3042    ///
3043    /// Each bidirectional pair appears exactly once, ordered so that
3044    /// `a < b` lexicographically.
3045    pub fn bidirectional_pairs(&self) -> Result<Vec<(EntityId, EntityId)>, AgentRuntimeError> {
3046        let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_pairs");
3047        let mut pairs: Vec<(EntityId, EntityId)> = Vec::new();
3048        for (from, rels) in &inner.adjacency {
3049            for rel in rels {
3050                let to = &rel.to;
3051                if from < to {
3052                    if inner
3053                        .adjacency
3054                        .get(to)
3055                        .map_or(false, |v| v.iter().any(|r| &r.to == from))
3056                    {
3057                        pairs.push((from.clone(), to.clone()));
3058                    }
3059                }
3060            }
3061        }
3062        Ok(pairs)
3063    }
3064
3065    /// Return the mean in-degree across all entities in the graph.
3066    ///
3067    /// Computed as `total_relationships / entity_count`.  Returns `0.0` when
3068    /// the graph has no entities.
3069    pub fn mean_in_degree(&self) -> Result<f64, AgentRuntimeError> {
3070        let inner = recover_lock(self.inner.lock(), "GraphStore::mean_in_degree");
3071        let entity_count = inner.entities.len();
3072        if entity_count == 0 {
3073            return Ok(0.0);
3074        }
3075        let total: usize = inner.reverse_adjacency.values().map(|v| v.len()).sum();
3076        Ok(total as f64 / entity_count as f64)
3077    }
3078
3079    /// Return the count of entities whose label starts with `prefix`.
3080    ///
3081    /// Returns `0` when no entities match or the graph is empty.
3082    pub fn entity_count_by_label_prefix(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
3083        let inner = recover_lock(
3084            self.inner.lock(),
3085            "GraphStore::entity_count_by_label_prefix",
3086        );
3087        Ok(inner.entities.values().filter(|e| e.label.starts_with(prefix)).count())
3088    }
3089
3090    /// Return the sum of all relationship weights in the graph.
3091    ///
3092    /// Returns `0.0` for an empty graph or when there are no relationships.
3093    pub fn relationship_weight_sum(&self) -> Result<f32, AgentRuntimeError> {
3094        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_weight_sum");
3095        Ok(inner
3096            .adjacency
3097            .values()
3098            .flat_map(|rels| rels.iter())
3099            .map(|r| r.weight)
3100            .sum())
3101    }
3102
3103    /// Return a frequency map of entity label → count.
3104    ///
3105    /// Returns an empty map for an empty graph.
3106    pub fn label_frequency(&self) -> Result<std::collections::HashMap<String, usize>, AgentRuntimeError> {
3107        let inner = recover_lock(self.inner.lock(), "GraphStore::label_frequency");
3108        let mut freq: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
3109        for entity in inner.entities.values() {
3110            *freq.entry(entity.label.clone()).or_insert(0) += 1;
3111        }
3112        Ok(freq)
3113    }
3114
3115    /// Return all entities sorted ascending by their ID string.
3116    ///
3117    /// Provides a stable, deterministic ordering that is independent of
3118    /// internal `HashMap` iteration order.  Returns an empty `Vec` for an
3119    /// empty graph.
3120    pub fn entities_sorted_by_id(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3121        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_id");
3122        let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
3123        entities.sort_unstable_by(|a, b| a.id.cmp(&b.id));
3124        Ok(entities)
3125    }
3126
3127    /// Return all entities whose label contains `substr` as a substring.
3128    ///
3129    /// Case-sensitive.  Useful for filtering entities by a partial label
3130    /// token (e.g. `"Person"` substring matches `"PersonA"` and `"PersonB"`).
3131    /// Returns an empty `Vec` for an empty graph or when no label matches.
3132    pub fn entities_with_label_containing(&self, substr: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3133        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_containing");
3134        Ok(inner.entities.values().filter(|e| e.label.contains(substr)).cloned().collect())
3135    }
3136
3137    /// Return the number of outgoing neighbors (out-degree) for `entity_id`.
3138    ///
3139    /// Returns `0` if the entity has no outgoing edges or does not exist.
3140    pub fn entity_neighbor_count(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
3141        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_neighbor_count");
3142        Ok(inner.adjacency.get(entity_id).map_or(0, |rels| rels.len()))
3143    }
3144
3145    /// Return all unique entity labels in alphabetical order.
3146    ///
3147    /// Deduplicates labels so each label appears only once.
3148    /// Returns an empty `Vec` for an empty graph.
3149    pub fn entity_labels_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
3150        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_labels_sorted");
3151        let mut labels: Vec<String> = inner
3152            .entities
3153            .values()
3154            .map(|e| e.label.clone())
3155            .collect::<std::collections::HashSet<_>>()
3156            .into_iter()
3157            .collect();
3158        labels.sort_unstable();
3159        Ok(labels)
3160    }
3161
3162    /// Return the number of unique relationship (edge) types in the graph.
3163    ///
3164    /// Counts distinct `kind` values across all relationships.
3165    /// Returns `0` for an empty graph.
3166    pub fn relationship_type_count(&self) -> Result<usize, AgentRuntimeError> {
3167        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_count");
3168        let kinds: std::collections::HashSet<&str> = inner
3169            .adjacency
3170            .values()
3171            .flat_map(|rels| rels.iter().map(|r| r.kind.as_str()))
3172            .collect();
3173        Ok(kinds.len())
3174    }
3175
3176    /// Return all entities whose label exactly matches `label` (case-sensitive).
3177    ///
3178    /// Unlike [`entities_with_label_containing`] this performs an exact match.
3179    /// Returns an empty `Vec` for an empty graph or when no entity matches.
3180    ///
3181    /// [`entities_with_label_containing`]: GraphStore::entities_with_label_containing
3182    pub fn entities_with_exact_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3183        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_exact_label");
3184        Ok(inner.entities.values().filter(|e| e.label == label).cloned().collect())
3185    }
3186
3187    /// Return all entity IDs present in the graph, sorted alphabetically.
3188    ///
3189    /// Returns an empty `Vec` for an empty graph.
3190    pub fn entity_ids_sorted(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
3191        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_sorted");
3192        let mut ids: Vec<EntityId> = inner.entities.keys().cloned().collect();
3193        ids.sort_by(|a, b| a.0.cmp(&b.0));
3194        Ok(ids)
3195    }
3196
3197    /// Return the number of outgoing relationships from `entity_id`.
3198    ///
3199    /// Returns `0` for an unknown entity or one with no outgoing edges.
3200    pub fn relationship_count_for(
3201        &self,
3202        entity_id: &EntityId,
3203    ) -> Result<usize, AgentRuntimeError> {
3204        let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_for");
3205        Ok(inner
3206            .adjacency
3207            .get(entity_id)
3208            .map_or(0, |rels| rels.len()))
3209    }
3210
3211    /// Return `true` if there is at least one directed edge from `from` to `to`.
3212    ///
3213    /// Returns `false` when either entity is unknown.
3214    pub fn entity_pair_has_relationship(
3215        &self,
3216        from: &EntityId,
3217        to: &EntityId,
3218    ) -> Result<bool, AgentRuntimeError> {
3219        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_pair_has_relationship");
3220        let to_id = to.clone();
3221        Ok(inner
3222            .adjacency
3223            .get(from)
3224            .map_or(false, |rels| rels.iter().any(|r| r.to == to_id)))
3225    }
3226
3227    /// Return all entity IDs reachable from `start` via directed edges
3228    /// (breadth-first), excluding `start` itself.
3229    ///
3230    /// Returns an empty `Vec` when `start` is unknown or has no outgoing edges.
3231    pub fn nodes_reachable_from(
3232        &self,
3233        start: &EntityId,
3234    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
3235        let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_reachable_from");
3236        let mut visited: std::collections::HashSet<EntityId> = std::collections::HashSet::new();
3237        let mut queue: std::collections::VecDeque<EntityId> = std::collections::VecDeque::new();
3238        queue.push_back(start.clone());
3239        while let Some(current) = queue.pop_front() {
3240            if !visited.insert(current.clone()) {
3241                continue;
3242            }
3243            if let Some(rels) = inner.adjacency.get(&current) {
3244                for r in rels {
3245                    if !visited.contains(&r.to) {
3246                        queue.push_back(r.to.clone());
3247                    }
3248                }
3249            }
3250        }
3251        visited.remove(start);
3252        let mut result: Vec<EntityId> = visited.into_iter().collect();
3253        result.sort_by(|a, b| a.0.cmp(&b.0));
3254        Ok(result)
3255    }
3256
3257    /// Return the average relationship weight across all edges in the graph.
3258    ///
3259    /// Returns `0.0` when the graph has no edges.
3260    pub fn average_weight(&self) -> Result<f64, AgentRuntimeError> {
3261        let inner = recover_lock(self.inner.lock(), "GraphStore::average_weight");
3262        let all: Vec<f64> = inner
3263            .adjacency
3264            .values()
3265            .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3266            .collect();
3267        if all.is_empty() {
3268            return Ok(0.0);
3269        }
3270        Ok(all.iter().sum::<f64>() / all.len() as f64)
3271    }
3272
3273    /// Return the maximum relationship weight in the graph, or `None` if there
3274    /// are no edges.
3275    pub fn max_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
3276        let inner = recover_lock(self.inner.lock(), "GraphStore::max_weight");
3277        Ok(inner
3278            .adjacency
3279            .values()
3280            .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3281            .reduce(f64::max))
3282    }
3283
3284    /// Return the weight of the first edge from `from` to `to`, or `None` if
3285    /// no such edge exists.
3286    pub fn edge_weight_between(
3287        &self,
3288        from: &EntityId,
3289        to: &EntityId,
3290    ) -> Result<Option<f64>, AgentRuntimeError> {
3291        let inner = recover_lock(self.inner.lock(), "GraphStore::edge_weight_between");
3292        let to_id = to.clone();
3293        Ok(inner
3294            .adjacency
3295            .get(from)
3296            .and_then(|rels| rels.iter().find(|r| r.to == to_id))
3297            .map(|r| r.weight as f64))
3298    }
3299
3300    /// Return the sum of all relationship weights in the graph.
3301    ///
3302    /// Returns `0.0` for a graph with no edges.
3303    pub fn total_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
3304        let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_weight");
3305        Ok(inner
3306            .adjacency
3307            .values()
3308            .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3309            .sum())
3310    }
3311
3312    /// Return the average weight of all edges with the given relationship `kind`.
3313    ///
3314    /// Returns `0.0` when no edges of that kind exist.
3315    pub fn avg_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
3316        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight_for_kind");
3317        let weights: Vec<f64> = inner
3318            .adjacency
3319            .values()
3320            .flat_map(|rels| {
3321                rels.iter()
3322                    .filter(|r| r.kind.as_str() == kind)
3323                    .map(|r| r.weight as f64)
3324            })
3325            .collect();
3326        if weights.is_empty() {
3327            return Ok(0.0);
3328        }
3329        Ok(weights.iter().sum::<f64>() / weights.len() as f64)
3330    }
3331
3332    /// Return the ratio of out-degree to total entity count for `entity_id`.
3333    ///
3334    /// Returns `0.0` when the graph is empty or the entity has no outgoing
3335    /// edges.
3336    pub fn entity_degree_ratio(&self, entity_id: &EntityId) -> Result<f64, AgentRuntimeError> {
3337        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_degree_ratio");
3338        let total = inner.entities.len();
3339        if total == 0 {
3340            return Ok(0.0);
3341        }
3342        let out_deg = inner
3343            .adjacency
3344            .get(entity_id)
3345            .map_or(0, |rels| rels.len());
3346        Ok(out_deg as f64 / total as f64)
3347    }
3348
3349    /// Return all entities whose out-degree is at least `min_degree`.
3350    /// Return all entities that have zero outgoing edges (sink nodes).
3351    ///
3352    /// Returns an empty `Vec` for an empty graph.
3353    pub fn nodes_with_no_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3354        let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_with_no_outgoing");
3355        Ok(inner
3356            .entities
3357            .values()
3358            .filter(|e| {
3359                inner
3360                    .adjacency
3361                    .get(&e.id)
3362                    .map_or(true, |rels| rels.is_empty())
3363            })
3364            .cloned()
3365            .collect())
3366    }
3367
3368    /// Return the in-degree of the entity with the given `id` — the number of
3369    /// relationships whose `to` field equals `id`.
3370    ///
3371    /// Returns `0` when the entity has no incoming edges or does not exist.
3372    pub fn in_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
3373        let inner = recover_lock(self.inner.lock(), "GraphStore::in_degree_of");
3374        let count = inner
3375            .adjacency
3376            .values()
3377            .flat_map(|rels| rels.iter())
3378            .filter(|r| &r.to == id)
3379            .count();
3380        Ok(count)
3381    }
3382
3383    /// Return the sum of weights of all relationships whose `kind` equals the
3384    /// given string.
3385    ///
3386    /// Returns `0.0` when no relationships of that kind exist.
3387    pub fn total_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
3388        let inner = recover_lock(self.inner.lock(), "GraphStore::total_weight_for_kind");
3389        let sum: f64 = inner
3390            .adjacency
3391            .values()
3392            .flat_map(|rels| rels.iter())
3393            .filter(|r| r.kind == kind)
3394            .map(|r| r.weight as f64)
3395            .sum();
3396        Ok(sum)
3397    }
3398
3399    /// Return the `EntityId`s of all entities with the given `label`.
3400    ///
3401    /// Returns an empty `Vec` when no matching entities exist.
3402    pub fn entity_ids_with_label(&self, label: &str) -> Result<Vec<EntityId>, AgentRuntimeError> {
3403        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_with_label");
3404        Ok(inner
3405            .entities
3406            .values()
3407            .filter(|e| e.label == label)
3408            .map(|e| e.id.clone())
3409            .collect())
3410    }
3411
3412    /// Return the number of distinct entity labels currently in the graph.
3413    pub fn labels_unique_count(&self) -> Result<usize, AgentRuntimeError> {
3414        let inner = recover_lock(self.inner.lock(), "GraphStore::labels_unique_count");
3415        let labels: std::collections::HashSet<&str> =
3416            inner.entities.values().map(|e| e.label.as_str()).collect();
3417        Ok(labels.len())
3418    }
3419
3420    /// Return all entities that have the given `property_key` set in their
3421    /// `properties` map.
3422    pub fn entities_with_property_key(
3423        &self,
3424        property_key: &str,
3425    ) -> Result<Vec<Entity>, AgentRuntimeError> {
3426        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property_key");
3427        Ok(inner
3428            .entities
3429            .values()
3430            .filter(|e| e.properties.contains_key(property_key))
3431            .cloned()
3432            .collect())
3433    }
3434
3435    /// Return the number of entities that have the given `property_key` set.
3436    pub fn entity_properties_count(&self, property_key: &str) -> Result<usize, AgentRuntimeError> {
3437        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_properties_count");
3438        Ok(inner
3439            .entities
3440            .values()
3441            .filter(|e| e.properties.contains_key(property_key))
3442            .count())
3443    }
3444
3445    /// Returns all relationships that point **to** the entity with the given `id`.
3446    pub fn edges_to(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
3447        let inner = recover_lock(self.inner.lock(), "GraphStore::edges_to");
3448        Ok(inner
3449            .adjacency
3450            .values()
3451            .flat_map(|rels| rels.iter())
3452            .filter(|r| &r.to == id)
3453            .cloned()
3454            .collect())
3455    }
3456
3457    /// Returns `true` if the entity's property `key` equals `value` (string comparison).
3458    pub fn entity_has_property_value(
3459        &self,
3460        id: &EntityId,
3461        key: &str,
3462        value: &str,
3463    ) -> Result<bool, AgentRuntimeError> {
3464        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_has_property_value");
3465        Ok(inner
3466            .entities
3467            .get(id)
3468            .and_then(|e| e.properties.get(key))
3469            .map_or(false, |v| v == value))
3470    }
3471
3472    /// Return all outgoing `Relationship`s from the entity with the given `id`.
3473    ///
3474    /// Returns an empty `Vec` when the entity has no outgoing edges or does
3475    /// not exist.
3476    pub fn relationships_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
3477        let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_from");
3478        Ok(inner
3479            .adjacency
3480            .get(id)
3481            .map(|rels| rels.clone())
3482            .unwrap_or_default())
3483    }
3484
3485    ///
3486    /// Entities with no outgoing edges have an out-degree of 0 and are
3487    /// excluded unless `min_degree` is 0.  Returns an empty `Vec` for an
3488    /// empty graph.
3489    pub fn entities_with_min_out_degree(
3490        &self,
3491        min_degree: usize,
3492    ) -> Result<Vec<Entity>, AgentRuntimeError> {
3493        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_out_degree");
3494        let entities: Vec<Entity> = inner
3495            .entities
3496            .values()
3497            .filter(|e| {
3498                inner
3499                    .adjacency
3500                    .get(&e.id)
3501                    .map_or(0, |rels| rels.len())
3502                    >= min_degree
3503            })
3504            .cloned()
3505            .collect();
3506        Ok(entities)
3507    }
3508
3509    /// Return all entities whose in-degree is at least `min_degree`.
3510    ///
3511    /// In-degree is the number of relationships that *target* the entity.
3512    /// Returns an empty `Vec` when no entity satisfies the threshold.
3513    pub fn entities_with_min_in_degree(
3514        &self,
3515        min_degree: usize,
3516    ) -> Result<Vec<Entity>, AgentRuntimeError> {
3517        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_in_degree");
3518        // Build an in-degree map.
3519        let mut in_degree: std::collections::HashMap<&EntityId, usize> =
3520            std::collections::HashMap::new();
3521        for rels in inner.adjacency.values() {
3522            for rel in rels {
3523                *in_degree.entry(&rel.to).or_insert(0) += 1;
3524            }
3525        }
3526        let entities: Vec<Entity> = inner
3527            .entities
3528            .values()
3529            .filter(|e| in_degree.get(&e.id).copied().unwrap_or(0) >= min_degree)
3530            .cloned()
3531            .collect();
3532        Ok(entities)
3533    }
3534
3535    /// Return the total number of properties across all entities in the graph.
3536    ///
3537    /// Counts every key-value pair in every entity's property map.
3538    /// Returns `0` for an empty graph or when no entity carries any properties.
3539    pub fn total_property_count(&self) -> Result<usize, AgentRuntimeError> {
3540        let inner = recover_lock(self.inner.lock(), "GraphStore::total_property_count");
3541        Ok(inner.entities.values().map(|e| e.property_count()).sum())
3542    }
3543
3544    /// Return all entities that have no properties.
3545    ///
3546    /// Useful for identifying bare "stub" nodes that have not yet been annotated.
3547    pub fn entities_with_no_properties(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3548        let inner =
3549            recover_lock(self.inner.lock(), "GraphStore::entities_with_no_properties");
3550        Ok(inner
3551            .entities
3552            .values()
3553            .filter(|e| e.properties_is_empty())
3554            .cloned()
3555            .collect())
3556    }
3557
3558    /// Return the fraction of relationships whose weight is strictly greater
3559    /// than `threshold`.
3560    ///
3561    /// Returns `0.0` when the graph has no relationships.
3562    pub fn weight_above_threshold_ratio(
3563        &self,
3564        threshold: f32,
3565    ) -> Result<f64, AgentRuntimeError> {
3566        let inner =
3567            recover_lock(self.inner.lock(), "GraphStore::weight_above_threshold_ratio");
3568        let all: Vec<f32> = inner
3569            .adjacency
3570            .values()
3571            .flat_map(|rels| rels.iter().map(|r| r.weight))
3572            .collect();
3573        if all.is_empty() {
3574            return Ok(0.0);
3575        }
3576        let above = all.iter().filter(|&&w| w > threshold).count();
3577        Ok(above as f64 / all.len() as f64)
3578    }
3579
3580    /// Return all entities sorted by out-degree in descending order.
3581    ///
3582    /// Entities with the most outgoing relationships appear first.  Ties are
3583    /// broken by entity ID in ascending lexicographic order.
3584    pub fn entities_sorted_by_out_degree(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3585        let inner =
3586            recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_out_degree");
3587        let mut pairs: Vec<(Entity, usize)> = inner
3588            .entities
3589            .values()
3590            .map(|e| {
3591                let degree = inner
3592                    .adjacency
3593                    .get(&e.id)
3594                    .map_or(0, |rels| rels.len());
3595                (e.clone(), degree)
3596            })
3597            .collect();
3598        pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.id.as_str().cmp(b.0.id.as_str())));
3599        Ok(pairs.into_iter().map(|(e, _)| e).collect())
3600    }
3601
3602    /// Return the first entity whose label exactly matches `label`, or `None`.
3603    ///
3604    /// When multiple entities share the same label the returned entity is
3605    /// arbitrary (HashMap iteration order).  Returns `None` for an empty graph.
3606    pub fn entity_by_label(&self, label: &str) -> Result<Option<Entity>, AgentRuntimeError> {
3607        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_by_label");
3608        Ok(inner.entities.values().find(|e| e.label == label).cloned())
3609    }
3610
3611    /// Return the number of distinct relationship kinds (types) in the graph.
3612    ///
3613    /// Returns `0` for an empty graph or one with no relationships.
3614    pub fn distinct_relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
3615        let inner = recover_lock(
3616            self.inner.lock(),
3617            "GraphStore::distinct_relationship_kind_count",
3618        );
3619        let kinds: std::collections::HashSet<&str> = inner
3620            .adjacency
3621            .values()
3622            .flat_map(|rels| rels.iter())
3623            .map(|r| r.kind.as_str())
3624            .collect();
3625        Ok(kinds.len())
3626    }
3627
3628    /// Return `true` if at least one entity in the graph has `label` as its
3629    /// label.
3630    ///
3631    /// Returns `false` for an empty graph.
3632    pub fn has_entity_with_label(&self, label: &str) -> Result<bool, AgentRuntimeError> {
3633        let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity_with_label");
3634        Ok(inner.entities.values().any(|e| e.label == label))
3635    }
3636
3637    /// Return the minimum edge weight across all relationships, or `None` if
3638    /// the graph has no relationships.
3639    pub fn min_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
3640        let inner = recover_lock(self.inner.lock(), "GraphStore::min_weight");
3641        let min = inner
3642            .adjacency
3643            .values()
3644            .flat_map(|rels| rels.iter())
3645            .map(|r| r.weight)
3646            .reduce(f32::min);
3647        Ok(min)
3648    }
3649
3650    /// Return the number of relationships whose `kind` field matches `kind`
3651    /// exactly (case-sensitive).
3652    ///
3653    /// Returns `0` when no relationships of that kind exist or the graph is
3654    /// empty.
3655    pub fn relationships_of_kind_count(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
3656        let inner = recover_lock(
3657            self.inner.lock(),
3658            "GraphStore::relationships_of_kind_count",
3659        );
3660        let count = inner
3661            .adjacency
3662            .values()
3663            .flat_map(|rels| rels.iter())
3664            .filter(|r| r.kind == kind)
3665            .count();
3666        Ok(count)
3667    }
3668
3669    /// Return the number of outgoing relationships from `entity_id`.
3670    ///
3671    /// Returns `0` when the entity has no outgoing edges or does not exist.
3672    pub fn relationship_count_for_entity(
3673        &self,
3674        entity_id: &EntityId,
3675    ) -> Result<usize, AgentRuntimeError> {
3676        let inner = recover_lock(
3677            self.inner.lock(),
3678            "GraphStore::relationship_count_for_entity",
3679        );
3680        Ok(inner
3681            .adjacency
3682            .get(entity_id)
3683            .map_or(0, |rels| rels.len()))
3684    }
3685
3686    /// Return the IDs of all entities whose `label` matches the given string.
3687    ///
3688    /// Returns an empty `Vec` when no entity with that label exists.
3689    pub fn entities_by_label(
3690        &self,
3691        label: &str,
3692    ) -> Result<Vec<EntityId>, AgentRuntimeError> {
3693        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_by_label");
3694        Ok(inner
3695            .entities
3696            .values()
3697            .filter(|e| e.label == label)
3698            .map(|e| e.id.clone())
3699            .collect())
3700    }
3701
3702    /// Return the number of edges from `from_id` that point to `to_id`.
3703    ///
3704    /// Returns `0` when either entity does not exist or no such edge is found.
3705    pub fn edge_count_between(
3706        &self,
3707        from_id: &EntityId,
3708        to_id: &EntityId,
3709    ) -> Result<usize, AgentRuntimeError> {
3710        let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_between");
3711        Ok(inner
3712            .adjacency
3713            .get(from_id)
3714            .map_or(0, |rels| rels.iter().filter(|r| &r.to == to_id).count()))
3715    }
3716
3717    /// Return `true` if there is at least one direct edge from `from_id` to
3718    /// `to_id`.
3719    ///
3720    /// Returns `false` when either entity does not exist or no direct edge is
3721    /// found.
3722    pub fn is_connected(
3723        &self,
3724        from_id: &EntityId,
3725        to_id: &EntityId,
3726    ) -> Result<bool, AgentRuntimeError> {
3727        let inner = recover_lock(self.inner.lock(), "GraphStore::is_connected");
3728        Ok(inner
3729            .adjacency
3730            .get(from_id)
3731            .map_or(false, |rels| rels.iter().any(|r| &r.to == to_id)))
3732    }
3733
3734    /// Return `true` if the graph contains no entities and no relationships.
3735    pub fn graph_is_empty(&self) -> Result<bool, AgentRuntimeError> {
3736        let inner = recover_lock(self.inner.lock(), "GraphStore::graph_is_empty");
3737        Ok(inner.entities.is_empty() && inner.adjacency.is_empty())
3738    }
3739
3740    /// Return a sorted list of all unique relationship kinds present in the
3741    /// graph.
3742    ///
3743    /// Returns an empty `Vec` for a graph with no relationships.
3744    pub fn unique_relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
3745        let inner =
3746            recover_lock(self.inner.lock(), "GraphStore::unique_relationship_kinds");
3747        let mut kinds: Vec<String> = inner
3748            .adjacency
3749            .values()
3750            .flat_map(|rels| rels.iter())
3751            .map(|r| r.kind.clone())
3752            .collect::<std::collections::HashSet<_>>()
3753            .into_iter()
3754            .collect();
3755        kinds.sort_unstable();
3756        Ok(kinds)
3757    }
3758
3759    /// Return `true` if the graph has at least one relationship.
3760    pub fn has_any_relationships(&self) -> Result<bool, AgentRuntimeError> {
3761        let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_relationships");
3762        Ok(inner.adjacency.values().any(|rels| !rels.is_empty()))
3763    }
3764
3765    /// Return the average weight across all relationships in the graph.
3766    ///
3767    /// Returns `0.0` for a graph with no relationships.
3768    pub fn avg_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
3769        let inner = recover_lock(self.inner.lock(), "GraphStore::avg_edge_weight");
3770        let rels: Vec<&crate::graph::Relationship> = inner
3771            .adjacency
3772            .values()
3773            .flat_map(|v| v.iter())
3774            .collect();
3775        if rels.is_empty() {
3776            return Ok(0.0);
3777        }
3778        let total: f64 = rels.iter().map(|r| r.weight as f64).sum();
3779        Ok(total / rels.len() as f64)
3780    }
3781
3782    /// Return all entities that have at least one **incoming** relationship
3783    /// (in-degree ≥ 1).
3784    ///
3785    /// Complements [`entities_without_incoming`].  Returns an empty `Vec` for
3786    /// a graph with no relationships.
3787    ///
3788    /// [`entities_without_incoming`]: GraphStore::entities_without_incoming
3789    pub fn entities_with_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3790        let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_incoming");
3791        let mut has_in: std::collections::HashSet<&EntityId> =
3792            std::collections::HashSet::new();
3793        for rels in inner.adjacency.values() {
3794            for r in rels {
3795                has_in.insert(&r.to);
3796            }
3797        }
3798        Ok(inner
3799            .entities
3800            .values()
3801            .filter(|e| has_in.contains(&e.id))
3802            .cloned()
3803            .collect())
3804    }
3805
3806    /// Return all entities that have no outgoing relationships in the graph.
3807    ///
3808    /// These are "sink" nodes — they may still have incoming edges from other
3809    /// entities but emit none themselves.
3810    pub fn entities_with_no_relationships(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3811        let inner = recover_lock(
3812            self.inner.lock(),
3813            "GraphStore::entities_with_no_relationships",
3814        );
3815        Ok(inner
3816            .entities
3817            .values()
3818            .filter(|e| {
3819                inner
3820                    .adjacency
3821                    .get(&e.id)
3822                    .map_or(true, |rels| rels.is_empty())
3823            })
3824            .cloned()
3825            .collect())
3826    }
3827
3828    /// Return the sum of all edge weights in the graph.
3829    ///
3830    /// Returns `0.0` for a graph with no relationships.
3831    pub fn total_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
3832        let inner = recover_lock(self.inner.lock(), "GraphStore::total_edge_weight");
3833        Ok(inner
3834            .adjacency
3835            .values()
3836            .flat_map(|rels| rels.iter())
3837            .map(|r| r.weight as f64)
3838            .sum())
3839    }
3840
3841    /// Return the entity with the highest out-degree (most outgoing edges).
3842    ///
3843    /// Returns `None` for an empty graph. When multiple entities share the
3844    /// maximum degree the first one encountered is returned.
3845    pub fn entity_with_max_out_degree(&self) -> Result<Option<Entity>, AgentRuntimeError> {
3846        let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_max_out_degree");
3847        Ok(inner
3848            .entities
3849            .values()
3850            .max_by_key(|e| {
3851                inner
3852                    .adjacency
3853                    .get(&e.id)
3854                    .map_or(0, |rels| rels.len())
3855            })
3856            .cloned())
3857    }
3858
3859    /// Return all self-loop relationships — where the `from` entity and the
3860    /// `to` entity are the same.
3861    ///
3862    /// Returns an empty `Vec` when no self-loops exist.
3863    pub fn self_loops(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
3864        let inner = recover_lock(self.inner.lock(), "GraphStore::self_loops");
3865        let loops: Vec<Relationship> = inner
3866            .adjacency
3867            .iter()
3868            .flat_map(|(from, rels)| {
3869                let from = from.clone();
3870                rels.iter()
3871                    .filter(move |r| r.to == from)
3872                    .cloned()
3873                    .collect::<Vec<_>>()
3874            })
3875            .collect();
3876        Ok(loops)
3877    }
3878
3879    /// Return entities that have the given `label` **and** have a property
3880    /// with key `key`.
3881    ///
3882    /// Returns an empty `Vec` when no entity matches both criteria.
3883    pub fn entities_with_label_and_property(
3884        &self,
3885        label: &str,
3886        key: &str,
3887    ) -> Result<Vec<Entity>, AgentRuntimeError> {
3888        let inner = recover_lock(
3889            self.inner.lock(),
3890            "GraphStore::entities_with_label_and_property",
3891        );
3892        Ok(inner
3893            .entities
3894            .values()
3895            .filter(|e| e.label == label && e.properties.contains_key(key))
3896            .cloned()
3897            .collect())
3898    }
3899
3900    /// Return `true` if the entity identified by `id` has at least one
3901    /// outgoing edge in the adjacency list.
3902    ///
3903    /// Returns `false` for unknown entity IDs as well as for nodes with no
3904    /// outgoing relationships.
3905    pub fn entity_has_outgoing_edge(
3906        &self,
3907        id: &EntityId,
3908    ) -> Result<bool, AgentRuntimeError> {
3909        let inner =
3910            recover_lock(self.inner.lock(), "GraphStore::entity_has_outgoing_edge");
3911        Ok(inner
3912            .adjacency
3913            .get(id)
3914            .map_or(false, |rels| !rels.is_empty()))
3915    }
3916
3917    /// Return the number of distinct nodes that have at least one self-loop
3918    /// (i.e. an edge from a node to itself).
3919    ///
3920    /// Complements [`GraphStore::self_loops`] which returns the full list of
3921    /// self-loop relationships.
3922    pub fn count_nodes_with_self_loop(&self) -> Result<usize, AgentRuntimeError> {
3923        let inner =
3924            recover_lock(self.inner.lock(), "GraphStore::count_nodes_with_self_loop");
3925        let count = inner
3926            .adjacency
3927            .iter()
3928            .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
3929            .count();
3930        Ok(count)
3931    }
3932
3933    /// Return the total number of self-loop edges in the graph — relationships
3934    /// where `from == to`.
3935    pub fn cycle_count(&self) -> Result<usize, AgentRuntimeError> {
3936        let inner = recover_lock(self.inner.lock(), "GraphStore::cycle_count");
3937        let count = inner
3938            .adjacency
3939            .iter()
3940            .flat_map(|(from, rels)| rels.iter().map(move |r| (from, r)))
3941            .filter(|(from, r)| &r.to == *from)
3942            .count();
3943        Ok(count)
3944    }
3945
3946    /// Return the out-degree (number of outgoing edges) for a specific entity.
3947    ///
3948    /// Returns `0` for unknown entity IDs.  Complements
3949    /// [`GraphStore::in_degree_of`] which counts incoming edges.
3950    pub fn out_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
3951        let inner =
3952            recover_lock(self.inner.lock(), "GraphStore::out_degree_of");
3953        Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
3954    }
3955
3956    /// Return `true` if the graph contains at least one relationship with the
3957    /// given `kind`.
3958    ///
3959    /// A cheaper alternative to [`GraphStore::relationships_of_kind_count`]
3960    /// when a boolean answer is sufficient.
3961    pub fn has_relationship_with_kind(
3962        &self,
3963        kind: &str,
3964    ) -> Result<bool, AgentRuntimeError> {
3965        let inner =
3966            recover_lock(self.inner.lock(), "GraphStore::has_relationship_with_kind");
3967        Ok(inner
3968            .adjacency
3969            .values()
3970            .flat_map(|rels| rels.iter())
3971            .any(|r| r.kind == kind))
3972    }
3973
3974    /// Return `true` if every entity in the graph has at least one entry in
3975    /// its `properties` map.
3976    ///
3977    /// Returns `true` vacuously when the graph contains no entities.
3978    pub fn all_entities_have_properties(&self) -> Result<bool, AgentRuntimeError> {
3979        let inner =
3980            recover_lock(self.inner.lock(), "GraphStore::all_entities_have_properties");
3981        Ok(inner.entities.values().all(|e| !e.properties.is_empty()))
3982    }
3983
3984}
3985
3986impl Default for GraphStore {
3987    fn default() -> Self {
3988        Self::new()
3989    }
3990}
3991
3992// ── Tests ─────────────────────────────────────────────────────────────────────
3993
3994#[cfg(test)]
3995mod tests {
3996    use super::*;
3997
3998    fn make_graph() -> GraphStore {
3999        GraphStore::new()
4000    }
4001
4002    fn add(g: &GraphStore, id: &str) {
4003        g.add_entity(Entity::new(id, "Node")).unwrap();
4004    }
4005
4006    fn link(g: &GraphStore, from: &str, to: &str) {
4007        g.add_relationship(Relationship::new(from, to, "CONNECTS", 1.0))
4008            .unwrap();
4009    }
4010
4011    fn link_w(g: &GraphStore, from: &str, to: &str, weight: f32) {
4012        g.add_relationship(Relationship::new(from, to, "CONNECTS", weight))
4013            .unwrap();
4014    }
4015
4016    // ── EntityId ──────────────────────────────────────────────────────────────
4017
4018    #[test]
4019    fn test_entity_id_equality() {
4020        assert_eq!(EntityId::new("a"), EntityId::new("a"));
4021        assert_ne!(EntityId::new("a"), EntityId::new("b"));
4022    }
4023
4024    #[test]
4025    fn test_entity_id_display() {
4026        let id = EntityId::new("hello");
4027        assert_eq!(id.to_string(), "hello");
4028    }
4029
4030    // ── Entity ────────────────────────────────────────────────────────────────
4031
4032    #[test]
4033    fn test_entity_new_has_empty_properties() {
4034        let e = Entity::new("e1", "Person");
4035        assert!(e.properties.is_empty());
4036    }
4037
4038    #[test]
4039    fn test_entity_with_properties_stores_props() {
4040        let mut props = HashMap::new();
4041        props.insert("age".into(), Value::Number(42.into()));
4042        let e = Entity::with_properties("e1", "Person", props);
4043        assert!(e.properties.contains_key("age"));
4044    }
4045
4046    // ── GraphStore basic ops ──────────────────────────────────────────────────
4047
4048    #[test]
4049    fn test_graph_add_entity_increments_count() {
4050        let g = make_graph();
4051        add(&g, "a");
4052        assert_eq!(g.entity_count().unwrap(), 1);
4053    }
4054
4055    #[test]
4056    fn test_graph_get_entity_returns_entity() {
4057        let g = make_graph();
4058        g.add_entity(Entity::new("e1", "Person")).unwrap();
4059        let e = g.get_entity(&EntityId::new("e1")).unwrap();
4060        assert_eq!(e.label, "Person");
4061    }
4062
4063    #[test]
4064    fn test_graph_get_entity_missing_returns_error() {
4065        let g = make_graph();
4066        assert!(g.get_entity(&EntityId::new("ghost")).is_err());
4067    }
4068
4069    #[test]
4070    fn test_graph_add_relationship_increments_count() {
4071        let g = make_graph();
4072        add(&g, "a");
4073        add(&g, "b");
4074        link(&g, "a", "b");
4075        assert_eq!(g.relationship_count().unwrap(), 1);
4076    }
4077
4078    #[test]
4079    fn test_graph_add_relationship_missing_source_fails() {
4080        let g = make_graph();
4081        add(&g, "b");
4082        let result = g.add_relationship(Relationship::new("ghost", "b", "X", 1.0));
4083        assert!(result.is_err());
4084    }
4085
4086    #[test]
4087    fn test_graph_add_relationship_missing_target_fails() {
4088        let g = make_graph();
4089        add(&g, "a");
4090        let result = g.add_relationship(Relationship::new("a", "ghost", "X", 1.0));
4091        assert!(result.is_err());
4092    }
4093
4094    #[test]
4095    fn test_graph_remove_entity_removes_relationships() {
4096        let g = make_graph();
4097        add(&g, "a");
4098        add(&g, "b");
4099        link(&g, "a", "b");
4100        g.remove_entity(&EntityId::new("a")).unwrap();
4101        assert_eq!(g.entity_count().unwrap(), 1);
4102        assert_eq!(g.relationship_count().unwrap(), 0);
4103    }
4104
4105    #[test]
4106    fn test_graph_remove_entity_missing_returns_error() {
4107        let g = make_graph();
4108        assert!(g.remove_entity(&EntityId::new("ghost")).is_err());
4109    }
4110
4111    // ── BFS ───────────────────────────────────────────────────────────────────
4112
4113    #[test]
4114    fn test_bfs_finds_direct_neighbours() {
4115        let g = make_graph();
4116        add(&g, "a");
4117        add(&g, "b");
4118        add(&g, "c");
4119        link(&g, "a", "b");
4120        link(&g, "a", "c");
4121        let visited = g.bfs(&EntityId::new("a")).unwrap();
4122        assert_eq!(visited.len(), 2);
4123    }
4124
4125    #[test]
4126    fn test_bfs_traverses_chain() {
4127        let g = make_graph();
4128        add(&g, "a");
4129        add(&g, "b");
4130        add(&g, "c");
4131        add(&g, "d");
4132        link(&g, "a", "b");
4133        link(&g, "b", "c");
4134        link(&g, "c", "d");
4135        let visited = g.bfs(&EntityId::new("a")).unwrap();
4136        assert_eq!(visited.len(), 3);
4137        assert_eq!(visited[0], EntityId::new("b"));
4138    }
4139
4140    #[test]
4141    fn test_bfs_handles_isolated_node() {
4142        let g = make_graph();
4143        add(&g, "a");
4144        let visited = g.bfs(&EntityId::new("a")).unwrap();
4145        assert!(visited.is_empty());
4146    }
4147
4148    #[test]
4149    fn test_bfs_missing_start_returns_error() {
4150        let g = make_graph();
4151        assert!(g.bfs(&EntityId::new("ghost")).is_err());
4152    }
4153
4154    // ── DFS ───────────────────────────────────────────────────────────────────
4155
4156    #[test]
4157    fn test_dfs_visits_all_reachable_nodes() {
4158        let g = make_graph();
4159        add(&g, "a");
4160        add(&g, "b");
4161        add(&g, "c");
4162        add(&g, "d");
4163        link(&g, "a", "b");
4164        link(&g, "a", "c");
4165        link(&g, "b", "d");
4166        let visited = g.dfs(&EntityId::new("a")).unwrap();
4167        assert_eq!(visited.len(), 3);
4168    }
4169
4170    #[test]
4171    fn test_dfs_handles_isolated_node() {
4172        let g = make_graph();
4173        add(&g, "a");
4174        let visited = g.dfs(&EntityId::new("a")).unwrap();
4175        assert!(visited.is_empty());
4176    }
4177
4178    #[test]
4179    fn test_dfs_missing_start_returns_error() {
4180        let g = make_graph();
4181        assert!(g.dfs(&EntityId::new("ghost")).is_err());
4182    }
4183
4184    // ── Shortest path ─────────────────────────────────────────────────────────
4185
4186    #[test]
4187    fn test_shortest_path_direct_connection() {
4188        let g = make_graph();
4189        add(&g, "a");
4190        add(&g, "b");
4191        link(&g, "a", "b");
4192        let path = g
4193            .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
4194            .unwrap();
4195        assert_eq!(path, Some(vec![EntityId::new("a"), EntityId::new("b")]));
4196    }
4197
4198    #[test]
4199    fn test_shortest_path_multi_hop() {
4200        let g = make_graph();
4201        add(&g, "a");
4202        add(&g, "b");
4203        add(&g, "c");
4204        link(&g, "a", "b");
4205        link(&g, "b", "c");
4206        let path = g
4207            .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
4208            .unwrap();
4209        assert_eq!(path.as_ref().map(|p| p.len()), Some(3));
4210    }
4211
4212    #[test]
4213    fn test_shortest_path_returns_none_for_disconnected() {
4214        let g = make_graph();
4215        add(&g, "a");
4216        add(&g, "b");
4217        let path = g
4218            .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
4219            .unwrap();
4220        assert_eq!(path, None);
4221    }
4222
4223    #[test]
4224    fn test_shortest_path_same_node_returns_single_element() {
4225        let g = make_graph();
4226        add(&g, "a");
4227        let path = g
4228            .shortest_path(&EntityId::new("a"), &EntityId::new("a"))
4229            .unwrap();
4230        assert_eq!(path, Some(vec![EntityId::new("a")]));
4231    }
4232
4233    #[test]
4234    fn test_shortest_path_missing_source_returns_error() {
4235        let g = make_graph();
4236        add(&g, "b");
4237        assert!(g
4238            .shortest_path(&EntityId::new("ghost"), &EntityId::new("b"))
4239            .is_err());
4240    }
4241
4242    #[test]
4243    fn test_shortest_path_missing_target_returns_error() {
4244        let g = make_graph();
4245        add(&g, "a");
4246        assert!(g
4247            .shortest_path(&EntityId::new("a"), &EntityId::new("ghost"))
4248            .is_err());
4249    }
4250
4251    // ── Transitive closure ────────────────────────────────────────────────────
4252
4253    #[test]
4254    fn test_transitive_closure_includes_start() {
4255        let g = make_graph();
4256        add(&g, "a");
4257        add(&g, "b");
4258        link(&g, "a", "b");
4259        let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
4260        assert!(closure.contains(&EntityId::new("a")));
4261        assert!(closure.contains(&EntityId::new("b")));
4262    }
4263
4264    #[test]
4265    fn test_transitive_closure_isolated_node_contains_only_self() {
4266        let g = make_graph();
4267        add(&g, "a");
4268        let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
4269        assert_eq!(closure.len(), 1);
4270    }
4271
4272    // ── MemGraphError conversion ──────────────────────────────────────────────
4273
4274    #[test]
4275    fn test_mem_graph_error_converts_to_runtime_error() {
4276        let e = MemGraphError::EntityNotFound("x".into());
4277        let re: AgentRuntimeError = e.into();
4278        assert!(matches!(re, AgentRuntimeError::Graph(_)));
4279    }
4280
4281    // ── Weighted shortest path ────────────────────────────────────────────────
4282
4283    #[test]
4284    fn test_shortest_path_weighted_simple() {
4285        // a --(1.0)--> b --(2.0)--> c
4286        // a --(10.0)--> c  (direct but heavier)
4287        let g = make_graph();
4288        add(&g, "a");
4289        add(&g, "b");
4290        add(&g, "c");
4291        link_w(&g, "a", "b", 1.0);
4292        link_w(&g, "b", "c", 2.0);
4293        g.add_relationship(Relationship::new("a", "c", "DIRECT", 10.0))
4294            .unwrap();
4295
4296        let result = g
4297            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("c"))
4298            .unwrap();
4299        assert!(result.is_some());
4300        let (path, weight) = result.unwrap();
4301        // The cheapest path is a -> b -> c with total weight 3.0
4302        assert_eq!(
4303            path,
4304            vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")]
4305        );
4306        assert!((weight - 3.0).abs() < 1e-5);
4307    }
4308
4309    #[test]
4310    fn test_shortest_path_weighted_returns_none_for_disconnected() {
4311        let g = make_graph();
4312        add(&g, "a");
4313        add(&g, "b");
4314        let result = g
4315            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"))
4316            .unwrap();
4317        assert!(result.is_none());
4318    }
4319
4320    #[test]
4321    fn test_shortest_path_weighted_same_node() {
4322        let g = make_graph();
4323        add(&g, "a");
4324        let result = g
4325            .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("a"))
4326            .unwrap();
4327        assert_eq!(result, Some((vec![EntityId::new("a")], 0.0)));
4328    }
4329
4330    #[test]
4331    fn test_shortest_path_weighted_negative_weight_errors() {
4332        let g = make_graph();
4333        add(&g, "a");
4334        add(&g, "b");
4335        g.add_relationship(Relationship::new("a", "b", "NEG", -1.0))
4336            .unwrap();
4337        let result = g.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"));
4338        assert!(result.is_err());
4339    }
4340
4341    // ── Degree centrality ─────────────────────────────────────────────────────
4342
4343    #[test]
4344    fn test_degree_centrality_basic() {
4345        // Star graph: a -> b, a -> c, a -> d
4346        // a: out=3, in=0 => (3+0)/(4-1) = 1.0
4347        // b: out=0, in=1 => (0+1)/3 = 0.333...
4348        let g = make_graph();
4349        add(&g, "a");
4350        add(&g, "b");
4351        add(&g, "c");
4352        add(&g, "d");
4353        link(&g, "a", "b");
4354        link(&g, "a", "c");
4355        link(&g, "a", "d");
4356
4357        let centrality = g.degree_centrality().unwrap();
4358        let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
4359        let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
4360
4361        assert!((a_cent - 1.0).abs() < 1e-5, "a centrality was {a_cent}");
4362        assert!(
4363            (b_cent - 1.0 / 3.0).abs() < 1e-5,
4364            "b centrality was {b_cent}"
4365        );
4366    }
4367
4368    // ── Betweenness centrality ────────────────────────────────────────────────
4369
4370    #[test]
4371    fn test_betweenness_centrality_chain() {
4372        // Chain: a -> b -> c -> d
4373        // b and c are on all paths from a to c, a to d, b to d
4374        // Node b and c should have higher centrality than a and d.
4375        let g = make_graph();
4376        add(&g, "a");
4377        add(&g, "b");
4378        add(&g, "c");
4379        add(&g, "d");
4380        link(&g, "a", "b");
4381        link(&g, "b", "c");
4382        link(&g, "c", "d");
4383
4384        let centrality = g.betweenness_centrality().unwrap();
4385        let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
4386        let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
4387        let c_cent = *centrality.get(&EntityId::new("c")).unwrap();
4388        let d_cent = *centrality.get(&EntityId::new("d")).unwrap();
4389
4390        // Endpoints should have 0 centrality; intermediate nodes should be > 0.
4391        assert!((a_cent).abs() < 1e-5, "expected a_cent ~ 0, got {a_cent}");
4392        assert!(b_cent > 0.0, "expected b_cent > 0, got {b_cent}");
4393        assert!(c_cent > 0.0, "expected c_cent > 0, got {c_cent}");
4394        assert!((d_cent).abs() < 1e-5, "expected d_cent ~ 0, got {d_cent}");
4395    }
4396
4397    // ── Label propagation communities ─────────────────────────────────────────
4398
4399    #[test]
4400    fn test_label_propagation_communities_two_clusters() {
4401        // Cluster 1: a <-> b <-> c (fully connected via bidirectional edges)
4402        // Cluster 2: x <-> y <-> z
4403        // No edges between clusters.
4404        let g = make_graph();
4405        for id in &["a", "b", "c", "x", "y", "z"] {
4406            add(&g, id);
4407        }
4408        // Cluster 1 (bidirectional via two directed edges each)
4409        link(&g, "a", "b");
4410        link(&g, "b", "a");
4411        link(&g, "b", "c");
4412        link(&g, "c", "b");
4413        link(&g, "a", "c");
4414        link(&g, "c", "a");
4415        // Cluster 2
4416        link(&g, "x", "y");
4417        link(&g, "y", "x");
4418        link(&g, "y", "z");
4419        link(&g, "z", "y");
4420        link(&g, "x", "z");
4421        link(&g, "z", "x");
4422
4423        let communities = g.label_propagation_communities(100).unwrap();
4424
4425        let label_a = communities[&EntityId::new("a")];
4426        let label_b = communities[&EntityId::new("b")];
4427        let label_c = communities[&EntityId::new("c")];
4428        let label_x = communities[&EntityId::new("x")];
4429        let label_y = communities[&EntityId::new("y")];
4430        let label_z = communities[&EntityId::new("z")];
4431
4432        // All nodes in cluster 1 share a label, all in cluster 2 share a label,
4433        // and the two clusters have different labels.
4434        assert_eq!(label_a, label_b, "a and b should be in same community");
4435        assert_eq!(label_b, label_c, "b and c should be in same community");
4436        assert_eq!(label_x, label_y, "x and y should be in same community");
4437        assert_eq!(label_y, label_z, "y and z should be in same community");
4438        assert_ne!(
4439            label_a, label_x,
4440            "cluster 1 and cluster 2 should be different communities"
4441        );
4442    }
4443
4444    // ── Subgraph extraction ───────────────────────────────────────────────────
4445
4446    #[test]
4447    fn test_subgraph_extracts_correct_nodes_and_edges() {
4448        // Full graph: a -> b -> c -> d
4449        // Subgraph of {a, b, c} should contain edges a->b and b->c but not c->d.
4450        let g = make_graph();
4451        add(&g, "a");
4452        add(&g, "b");
4453        add(&g, "c");
4454        add(&g, "d");
4455        link(&g, "a", "b");
4456        link(&g, "b", "c");
4457        link(&g, "c", "d");
4458
4459        let sub = g
4460            .subgraph(&[EntityId::new("a"), EntityId::new("b"), EntityId::new("c")])
4461            .unwrap();
4462
4463        assert_eq!(sub.entity_count().unwrap(), 3);
4464        assert_eq!(sub.relationship_count().unwrap(), 2);
4465
4466        // d should not be present in the subgraph.
4467        assert!(sub.get_entity(&EntityId::new("d")).is_err());
4468
4469        // a -> b and b -> c should be present; c -> d should not.
4470        let path = sub
4471            .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
4472            .unwrap();
4473        assert!(path.is_some());
4474        assert_eq!(path.unwrap().len(), 3);
4475    }
4476
4477    // ── detect_cycles ──────────────────────────────────────────────────────────
4478
4479    #[test]
4480    fn test_detect_cycles_dag_returns_false() {
4481        let g = make_graph();
4482        add(&g, "a");
4483        add(&g, "b");
4484        add(&g, "c");
4485        link(&g, "a", "b");
4486        link(&g, "b", "c");
4487        assert_eq!(g.detect_cycles().unwrap(), false);
4488    }
4489
4490    #[test]
4491    fn test_detect_cycles_self_loop_returns_true() {
4492        let g = make_graph();
4493        add(&g, "a");
4494        // Use a different kind to avoid duplicate-relationship rejection.
4495        g.add_relationship(Relationship::new("a", "a", "SELF", 1.0))
4496            .unwrap();
4497        assert_eq!(g.detect_cycles().unwrap(), true);
4498    }
4499
4500    #[test]
4501    fn test_detect_cycles_simple_cycle_returns_true() {
4502        let g = make_graph();
4503        add(&g, "a");
4504        add(&g, "b");
4505        link(&g, "a", "b");
4506        g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
4507            .unwrap();
4508        assert_eq!(g.detect_cycles().unwrap(), true);
4509    }
4510
4511    #[test]
4512    fn test_detect_cycles_empty_graph_returns_false() {
4513        let g = make_graph();
4514        assert_eq!(g.detect_cycles().unwrap(), false);
4515    }
4516
4517    #[test]
4518    fn test_detect_cycles_result_is_cached() {
4519        let g = make_graph();
4520        add(&g, "x");
4521        add(&g, "y");
4522        link(&g, "x", "y");
4523        // First call.
4524        let r1 = g.detect_cycles().unwrap();
4525        // Second call should return the cached value.
4526        let r2 = g.detect_cycles().unwrap();
4527        assert_eq!(r1, r2);
4528    }
4529
4530    #[test]
4531    fn test_detect_cycles_cache_invalidated_on_mutation() {
4532        let g = make_graph();
4533        add(&g, "a");
4534        add(&g, "b");
4535        link(&g, "a", "b");
4536        assert_eq!(g.detect_cycles().unwrap(), false);
4537
4538        // Add a back edge to create a cycle — cache must be invalidated.
4539        g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
4540            .unwrap();
4541        assert_eq!(
4542            g.detect_cycles().unwrap(),
4543            true,
4544            "cache should be invalidated after adding a back edge"
4545        );
4546    }
4547
4548    // ── bfs_bounded / dfs_bounded ─────────────────────────────────────────────
4549
4550    #[test]
4551    fn test_bfs_bounded_respects_max_depth() {
4552        // Chain: a -> b -> c -> d
4553        let g = make_graph();
4554        add(&g, "a");
4555        add(&g, "b");
4556        add(&g, "c");
4557        add(&g, "d");
4558        link(&g, "a", "b");
4559        link(&g, "b", "c");
4560        link(&g, "c", "d");
4561
4562        // max_depth=1 should only visit a and b (depth 0 and 1)
4563        let visited = g.bfs_bounded("a", 1, 100).unwrap();
4564        assert!(visited.contains(&EntityId::new("a")));
4565        assert!(visited.contains(&EntityId::new("b")));
4566        assert!(!visited.contains(&EntityId::new("c")), "c is at depth 2, should not be visited");
4567    }
4568
4569    // ── #5/#35 path_exists ────────────────────────────────────────────────────
4570
4571    #[test]
4572    fn test_path_exists_returns_true() {
4573        let g = make_graph();
4574        add(&g, "a");
4575        add(&g, "b");
4576        add(&g, "c");
4577        link(&g, "a", "b");
4578        link(&g, "b", "c");
4579        assert_eq!(g.path_exists("a", "c").unwrap(), true);
4580    }
4581
4582    #[test]
4583    fn test_path_exists_returns_false() {
4584        let g = make_graph();
4585        add(&g, "a");
4586        add(&g, "b");
4587        assert_eq!(g.path_exists("a", "b").unwrap(), false);
4588    }
4589
4590    // ── #13 EntityId::as_str ──────────────────────────────────────────────────
4591
4592    #[test]
4593    fn test_entity_id_as_str() {
4594        let id = EntityId::new("my-entity");
4595        assert_eq!(id.as_str(), "my-entity");
4596    }
4597
4598    // ── #38 EntityId AsRef<str> ───────────────────────────────────────────────
4599
4600    #[test]
4601    fn test_entity_id_as_ref_str() {
4602        let id = EntityId::new("asref-test");
4603        let s: &str = id.as_ref();
4604        assert_eq!(s, "asref-test");
4605    }
4606
4607    #[test]
4608    fn test_dfs_bounded_respects_max_nodes() {
4609        // Chain: a -> b -> c -> d
4610        let g = make_graph();
4611        add(&g, "a");
4612        add(&g, "b");
4613        add(&g, "c");
4614        add(&g, "d");
4615        link(&g, "a", "b");
4616        link(&g, "b", "c");
4617        link(&g, "c", "d");
4618
4619        // max_nodes=2 means only 2 nodes total
4620        let visited = g.dfs_bounded("a", 100, 2).unwrap();
4621        assert_eq!(visited.len(), 2, "should stop at 2 nodes");
4622    }
4623
4624    // ── New API tests (Rounds 4-8) ────────────────────────────────────────────
4625
4626    #[test]
4627    fn test_entity_exists_and_relationship_exists() {
4628        let g = GraphStore::new();
4629        let a = EntityId::new("a");
4630        let b = EntityId::new("b");
4631        assert!(!g.entity_exists(&a).unwrap());
4632        g.add_entity(Entity::new("a", "Node")).unwrap();
4633        assert!(g.entity_exists(&a).unwrap());
4634        assert!(!g.relationship_exists(&a, &b, "knows").unwrap());
4635        g.add_entity(Entity::new("b", "Node")).unwrap();
4636        g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
4637        assert!(g.relationship_exists(&a, &b, "knows").unwrap());
4638        assert!(!g.relationship_exists(&a, &b, "likes").unwrap());
4639    }
4640
4641    #[test]
4642    fn test_get_relationships_for_returns_outgoing() {
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        let a = EntityId::new("a");
4648        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4649        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4650        let rels = g.get_relationships_for(&a).unwrap();
4651        assert_eq!(rels.len(), 2);
4652    }
4653
4654    #[test]
4655    fn test_relationships_between_finds_both_directions() {
4656        let g = GraphStore::new();
4657        g.add_entity(Entity::new("x", "N")).unwrap();
4658        g.add_entity(Entity::new("y", "N")).unwrap();
4659        let x = EntityId::new("x");
4660        let y = EntityId::new("y");
4661        g.add_relationship(Relationship::new("x", "y", "follows", 1.0)).unwrap();
4662        g.add_relationship(Relationship::new("y", "x", "blocks", 1.0)).unwrap();
4663        let rels = g.relationships_between(&x, &y).unwrap();
4664        assert_eq!(rels.len(), 2);
4665    }
4666
4667    #[test]
4668    fn test_find_entities_by_property() {
4669        let g = GraphStore::new();
4670        g.add_entity(Entity::new("a", "Person").with_property("age", serde_json::json!(30))).unwrap();
4671        g.add_entity(Entity::new("b", "Person").with_property("age", serde_json::json!(25))).unwrap();
4672        g.add_entity(Entity::new("c", "Person").with_property("age", serde_json::json!(30))).unwrap();
4673        let found = g.find_entities_by_property("age", &serde_json::json!(30)).unwrap();
4674        assert_eq!(found.len(), 2);
4675        let ids: Vec<_> = found.iter().map(|e| e.id.as_str()).collect();
4676        assert!(ids.contains(&"a") && ids.contains(&"c"));
4677    }
4678
4679    #[test]
4680    fn test_neighbor_entities_returns_entity_objects() {
4681        let g = GraphStore::new();
4682        g.add_entity(Entity::new("root", "R")).unwrap();
4683        g.add_entity(Entity::new("child1", "C")).unwrap();
4684        g.add_entity(Entity::new("child2", "C")).unwrap();
4685        let root = EntityId::new("root");
4686        g.add_relationship(Relationship::new("root", "child1", "has", 1.0)).unwrap();
4687        g.add_relationship(Relationship::new("root", "child2", "has", 1.0)).unwrap();
4688        let neighbors = g.neighbor_entities(&root).unwrap();
4689        assert_eq!(neighbors.len(), 2);
4690        let labels: Vec<_> = neighbors.iter().map(|e| e.label.as_str()).collect();
4691        assert!(labels.iter().all(|l| *l == "C"));
4692    }
4693
4694    #[test]
4695    fn test_remove_all_relationships_for() {
4696        let g = GraphStore::new();
4697        g.add_entity(Entity::new("a", "N")).unwrap();
4698        g.add_entity(Entity::new("b", "N")).unwrap();
4699        let a = EntityId::new("a");
4700        g.add_relationship(Relationship::new("a", "b", "r1", 1.0)).unwrap();
4701        g.add_relationship(Relationship::new("a", "b", "r2", 1.0)).unwrap();
4702        let removed = g.remove_all_relationships_for(&a).unwrap();
4703        assert_eq!(removed, 2);
4704        assert_eq!(g.relationship_count().unwrap(), 0);
4705    }
4706
4707    #[test]
4708    fn test_topological_sort_returns_valid_order() {
4709        let g = GraphStore::new();
4710        for id in &["a", "b", "c", "d"] {
4711            g.add_entity(Entity::new(*id, "N")).unwrap();
4712        }
4713        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4714        g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
4715        g.add_relationship(Relationship::new("c", "d", "r", 1.0)).unwrap();
4716        let order = g.topological_sort().unwrap();
4717        assert_eq!(order.len(), 4);
4718        let pos: std::collections::HashMap<_, _> = order.iter().enumerate().map(|(i, id)| (id.as_str().to_owned(), i)).collect();
4719        assert!(pos["a"] < pos["b"]);
4720        assert!(pos["b"] < pos["c"]);
4721        assert!(pos["c"] < pos["d"]);
4722    }
4723
4724    #[test]
4725    fn test_topological_sort_rejects_cycle() {
4726        let g = GraphStore::new();
4727        g.add_entity(Entity::new("x", "N")).unwrap();
4728        g.add_entity(Entity::new("y", "N")).unwrap();
4729        g.add_relationship(Relationship::new("x", "y", "r", 1.0)).unwrap();
4730        g.add_relationship(Relationship::new("y", "x", "r", 1.0)).unwrap();
4731        assert!(g.topological_sort().is_err());
4732    }
4733
4734    #[test]
4735    fn test_entity_count_by_label() {
4736        let g = GraphStore::new();
4737        g.add_entity(Entity::new("a", "Person")).unwrap();
4738        g.add_entity(Entity::new("b", "Person")).unwrap();
4739        g.add_entity(Entity::new("c", "Organization")).unwrap();
4740        assert_eq!(g.entity_count_by_label("Person").unwrap(), 2);
4741        assert_eq!(g.entity_count_by_label("Organization").unwrap(), 1);
4742        assert_eq!(g.entity_count_by_label("Unknown").unwrap(), 0);
4743    }
4744
4745    #[test]
4746    fn test_update_entity_label_and_property() {
4747        let g = GraphStore::new();
4748        let id = EntityId::new("e1");
4749        g.add_entity(Entity::new("e1", "Old")).unwrap();
4750        assert!(g.update_entity_label(&id, "New").unwrap());
4751        assert_eq!(g.get_entity(&id).unwrap().label, "New");
4752        assert!(g.update_entity_property(&id, "key", serde_json::json!("val")).unwrap());
4753        assert_eq!(g.get_entity(&id).unwrap().properties["key"], serde_json::json!("val"));
4754    }
4755
4756    #[test]
4757    fn test_connected_components_single_node() {
4758        let g = GraphStore::new();
4759        g.add_entity(Entity::new("a", "A")).unwrap();
4760        assert_eq!(g.connected_components().unwrap(), 1);
4761    }
4762
4763    #[test]
4764    fn test_connected_components_two_isolated_nodes() {
4765        let g = GraphStore::new();
4766        g.add_entity(Entity::new("a", "A")).unwrap();
4767        g.add_entity(Entity::new("b", "B")).unwrap();
4768        assert_eq!(g.connected_components().unwrap(), 2);
4769    }
4770
4771    #[test]
4772    fn test_connected_components_connected_pair() {
4773        let g = GraphStore::new();
4774        g.add_entity(Entity::new("a", "A")).unwrap();
4775        g.add_entity(Entity::new("b", "B")).unwrap();
4776        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4777        assert_eq!(g.connected_components().unwrap(), 1);
4778    }
4779
4780    #[test]
4781    fn test_connected_components_two_separate_pairs() {
4782        let g = GraphStore::new();
4783        g.add_entity(Entity::new("a", "A")).unwrap();
4784        g.add_entity(Entity::new("b", "B")).unwrap();
4785        g.add_entity(Entity::new("c", "C")).unwrap();
4786        g.add_entity(Entity::new("d", "D")).unwrap();
4787        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4788        g.add_relationship(Relationship::new("c", "d", "link", 1.0)).unwrap();
4789        assert_eq!(g.connected_components().unwrap(), 2);
4790    }
4791
4792    #[test]
4793    fn test_connected_components_empty_graph() {
4794        let g = GraphStore::new();
4795        assert_eq!(g.connected_components().unwrap(), 0);
4796    }
4797
4798    // ── Round 9: weakly_connected ─────────────────────────────────────────────
4799
4800    #[test]
4801    fn test_weakly_connected_true_for_empty_graph() {
4802        let g = GraphStore::new();
4803        assert!(g.weakly_connected().unwrap());
4804    }
4805
4806    #[test]
4807    fn test_weakly_connected_true_for_single_node() {
4808        let g = GraphStore::new();
4809        g.add_entity(Entity::new("a", "A")).unwrap();
4810        assert!(g.weakly_connected().unwrap());
4811    }
4812
4813    #[test]
4814    fn test_weakly_connected_true_when_all_nodes_connected() {
4815        let g = GraphStore::new();
4816        g.add_entity(Entity::new("a", "A")).unwrap();
4817        g.add_entity(Entity::new("b", "B")).unwrap();
4818        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4819        assert!(g.weakly_connected().unwrap());
4820    }
4821
4822    #[test]
4823    fn test_weakly_connected_false_when_nodes_isolated() {
4824        let g = GraphStore::new();
4825        g.add_entity(Entity::new("a", "A")).unwrap();
4826        g.add_entity(Entity::new("b", "B")).unwrap();
4827        assert!(!g.weakly_connected().unwrap());
4828    }
4829
4830    #[test]
4831    fn test_isolates_returns_nodes_with_no_edges() {
4832        let g = GraphStore::new();
4833        g.add_entity(Entity::new("a", "A")).unwrap();
4834        g.add_entity(Entity::new("b", "B")).unwrap();
4835        g.add_entity(Entity::new("c", "C")).unwrap();
4836        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4837        let iso = g.isolates().unwrap();
4838        let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
4839        ids.sort();
4840        assert_eq!(ids, vec!["c".to_string()]);
4841    }
4842
4843    #[test]
4844    fn test_isolates_returns_empty_when_all_connected() {
4845        let g = GraphStore::new();
4846        g.add_entity(Entity::new("a", "A")).unwrap();
4847        g.add_entity(Entity::new("b", "B")).unwrap();
4848        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4849        assert!(g.isolates().unwrap().is_empty());
4850    }
4851
4852    #[test]
4853    fn test_isolates_all_isolated() {
4854        let g = GraphStore::new();
4855        g.add_entity(Entity::new("x", "X")).unwrap();
4856        g.add_entity(Entity::new("y", "Y")).unwrap();
4857        let iso = g.isolates().unwrap();
4858        let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
4859        ids.sort();
4860        assert_eq!(ids, vec!["x".to_string(), "y".to_string()]);
4861    }
4862
4863    #[test]
4864    fn test_is_dag_on_dag() {
4865        let g = GraphStore::new();
4866        g.add_entity(Entity::new("a", "A")).unwrap();
4867        g.add_entity(Entity::new("b", "B")).unwrap();
4868        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4869        assert!(g.is_dag().unwrap());
4870    }
4871
4872    #[test]
4873    fn test_is_dag_on_cyclic_graph() {
4874        let g = GraphStore::new();
4875        g.add_entity(Entity::new("a", "A")).unwrap();
4876        g.add_entity(Entity::new("b", "B")).unwrap();
4877        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4878        g.add_relationship(Relationship::new("b", "a", "back", 1.0)).unwrap();
4879        assert!(!g.is_dag().unwrap());
4880    }
4881
4882    #[test]
4883    fn test_in_degree_and_out_degree() {
4884        let g = GraphStore::new();
4885        g.add_entity(Entity::new("a", "A")).unwrap();
4886        g.add_entity(Entity::new("b", "B")).unwrap();
4887        g.add_entity(Entity::new("c", "C")).unwrap();
4888        g.add_relationship(Relationship::new("a", "b", "e1", 1.0)).unwrap();
4889        g.add_relationship(Relationship::new("c", "b", "e2", 1.0)).unwrap();
4890        let a = EntityId::new("a");
4891        let b = EntityId::new("b");
4892        let c = EntityId::new("c");
4893        assert_eq!(g.out_degree(&a).unwrap(), 1);
4894        assert_eq!(g.in_degree(&a).unwrap(), 0);
4895        assert_eq!(g.in_degree(&b).unwrap(), 2);
4896        assert_eq!(g.out_degree(&b).unwrap(), 0);
4897        assert_eq!(g.out_degree(&c).unwrap(), 1);
4898        assert_eq!(g.in_degree(&c).unwrap(), 0);
4899    }
4900
4901    #[test]
4902    fn test_in_degree_missing_entity_returns_zero() {
4903        let g = GraphStore::new();
4904        let id = EntityId::new("ghost");
4905        assert_eq!(g.in_degree(&id).unwrap(), 0);
4906    }
4907
4908    #[test]
4909    fn test_out_degree_missing_entity_returns_zero() {
4910        let g = GraphStore::new();
4911        let id = EntityId::new("ghost");
4912        assert_eq!(g.out_degree(&id).unwrap(), 0);
4913    }
4914
4915    #[test]
4916    fn test_node_count_is_alias_for_entity_count() {
4917        let g = GraphStore::new();
4918        g.add_entity(Entity::new("a", "A")).unwrap();
4919        g.add_entity(Entity::new("b", "B")).unwrap();
4920        assert_eq!(g.node_count().unwrap(), g.entity_count().unwrap());
4921        assert_eq!(g.node_count().unwrap(), 2);
4922    }
4923
4924    #[test]
4925    fn test_edge_count_is_alias_for_relationship_count() {
4926        let g = GraphStore::new();
4927        g.add_entity(Entity::new("a", "A")).unwrap();
4928        g.add_entity(Entity::new("b", "B")).unwrap();
4929        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4930        assert_eq!(g.edge_count().unwrap(), g.relationship_count().unwrap());
4931        assert_eq!(g.edge_count().unwrap(), 1);
4932    }
4933
4934    #[test]
4935    fn test_source_nodes_returns_nodes_with_no_incoming_edges() {
4936        let g = GraphStore::new();
4937        g.add_entity(Entity::new("root", "Root")).unwrap();
4938        g.add_entity(Entity::new("child", "Child")).unwrap();
4939        g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
4940        g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
4941        g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
4942        let sources = g.source_nodes().unwrap();
4943        assert_eq!(sources.len(), 1);
4944        assert_eq!(sources[0].id.as_str(), "root");
4945    }
4946
4947    #[test]
4948    fn test_sink_nodes_returns_nodes_with_no_outgoing_edges() {
4949        let g = GraphStore::new();
4950        g.add_entity(Entity::new("root", "Root")).unwrap();
4951        g.add_entity(Entity::new("child", "Child")).unwrap();
4952        g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
4953        g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
4954        g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
4955        let sinks = g.sink_nodes().unwrap();
4956        assert_eq!(sinks.len(), 1);
4957        assert_eq!(sinks[0].id.as_str(), "leaf");
4958    }
4959
4960    #[test]
4961    fn test_source_and_sink_empty_on_isolated_node() {
4962        // An isolated node has no in or out edges, so it's both source and sink
4963        let g = GraphStore::new();
4964        g.add_entity(Entity::new("solo", "Solo")).unwrap();
4965        assert_eq!(g.source_nodes().unwrap().len(), 1);
4966        assert_eq!(g.sink_nodes().unwrap().len(), 1);
4967    }
4968
4969    #[test]
4970    fn test_reverse_flips_all_edges() {
4971        let g = GraphStore::new();
4972        g.add_entity(Entity::new("a", "A")).unwrap();
4973        g.add_entity(Entity::new("b", "B")).unwrap();
4974        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4975        let rev = g.reverse().unwrap();
4976        // Original: a→b. Reversed: b→a.
4977        assert_eq!(rev.entity_count().unwrap(), 2);
4978        assert_eq!(rev.relationship_count().unwrap(), 1);
4979        let b_id = EntityId::new("b");
4980        let a_id = EntityId::new("a");
4981        assert!(rev.relationship_exists(&b_id, &a_id, "edge").unwrap());
4982    }
4983
4984    #[test]
4985    fn test_reverse_empty_graph_stays_empty() {
4986        let g = GraphStore::new();
4987        let rev = g.reverse().unwrap();
4988        assert_eq!(rev.entity_count().unwrap(), 0);
4989    }
4990
4991    #[test]
4992    fn test_common_neighbors_finds_shared_targets() {
4993        let g = GraphStore::new();
4994        g.add_entity(Entity::new("a", "A")).unwrap();
4995        g.add_entity(Entity::new("b", "B")).unwrap();
4996        g.add_entity(Entity::new("shared", "S")).unwrap();
4997        g.add_entity(Entity::new("only_a", "OA")).unwrap();
4998        g.add_relationship(Relationship::new("a", "shared", "e", 1.0)).unwrap();
4999        g.add_relationship(Relationship::new("b", "shared", "e", 1.0)).unwrap();
5000        g.add_relationship(Relationship::new("a", "only_a", "e", 1.0)).unwrap();
5001        let a_id = EntityId::new("a");
5002        let b_id = EntityId::new("b");
5003        let common = g.common_neighbors(&a_id, &b_id).unwrap();
5004        assert_eq!(common.len(), 1);
5005        assert_eq!(common[0].id.as_str(), "shared");
5006    }
5007
5008    #[test]
5009    fn test_common_neighbors_empty_when_none_shared() {
5010        let g = GraphStore::new();
5011        g.add_entity(Entity::new("a", "A")).unwrap();
5012        g.add_entity(Entity::new("b", "B")).unwrap();
5013        g.add_entity(Entity::new("x", "X")).unwrap();
5014        g.add_entity(Entity::new("y", "Y")).unwrap();
5015        g.add_relationship(Relationship::new("a", "x", "e", 1.0)).unwrap();
5016        g.add_relationship(Relationship::new("b", "y", "e", 1.0)).unwrap();
5017        let a_id = EntityId::new("a");
5018        let b_id = EntityId::new("b");
5019        assert!(g.common_neighbors(&a_id, &b_id).unwrap().is_empty());
5020    }
5021
5022    // ── Round 3: entity_ids, is_empty, clear ─────────────────────────────────
5023
5024    #[test]
5025    fn test_graph_is_empty_initially() {
5026        let g = GraphStore::new();
5027        assert!(g.is_empty().unwrap());
5028    }
5029
5030    #[test]
5031    fn test_graph_is_empty_false_after_add() {
5032        let g = GraphStore::new();
5033        g.add_entity(Entity::new("a", "A")).unwrap();
5034        assert!(!g.is_empty().unwrap());
5035    }
5036
5037    #[test]
5038    fn test_graph_entity_ids_returns_all_ids() {
5039        let g = GraphStore::new();
5040        g.add_entity(Entity::new("x", "X")).unwrap();
5041        g.add_entity(Entity::new("y", "Y")).unwrap();
5042        let ids = g.entity_ids().unwrap();
5043        assert_eq!(ids.len(), 2);
5044        assert!(ids.iter().any(|id| id.0 == "x"));
5045        assert!(ids.iter().any(|id| id.0 == "y"));
5046    }
5047
5048    #[test]
5049    fn test_graph_clear_removes_entities_and_relationships() {
5050        let g = GraphStore::new();
5051        g.add_entity(Entity::new("a", "A")).unwrap();
5052        g.add_entity(Entity::new("b", "B")).unwrap();
5053        g.add_relationship(Relationship::new("a", "b", "links", 1.0))
5054        .unwrap();
5055        g.clear().unwrap();
5056        assert_eq!(g.entity_count().unwrap(), 0);
5057        assert_eq!(g.relationship_count().unwrap(), 0);
5058        assert!(g.is_empty().unwrap());
5059    }
5060
5061    // ── Round 16: weight_of, neighbors_in, path_exists ───────────────────────
5062
5063    #[test]
5064    fn test_weight_of_returns_edge_weight() {
5065        let g = make_graph();
5066        add(&g, "x"); add(&g, "y");
5067        link_w(&g, "x", "y", 3.5);
5068        let w = g.weight_of(&EntityId::new("x"), &EntityId::new("y")).unwrap();
5069        assert!(w.is_some());
5070        assert!((w.unwrap() - 3.5).abs() < 1e-6);
5071    }
5072
5073    #[test]
5074    fn test_weight_of_absent_edge_returns_none() {
5075        let g = make_graph();
5076        add(&g, "a"); add(&g, "b");
5077        let w = g.weight_of(&EntityId::new("a"), &EntityId::new("b")).unwrap();
5078        assert!(w.is_none());
5079    }
5080
5081    #[test]
5082    fn test_neighbors_in_returns_predecessors() {
5083        let g = make_graph();
5084        add(&g, "a"); add(&g, "b"); add(&g, "c");
5085        link(&g, "a", "c"); link(&g, "b", "c");
5086        let mut preds: Vec<String> = g
5087            .neighbors_in(&EntityId::new("c"))
5088            .unwrap()
5089            .into_iter()
5090            .map(|id| id.as_str().to_string())
5091            .collect();
5092        preds.sort();
5093        assert_eq!(preds, vec!["a", "b"]);
5094    }
5095
5096    #[test]
5097    fn test_neighbors_in_empty_for_node_with_no_incoming() {
5098        let g = make_graph();
5099        add(&g, "isolated");
5100        let preds = g.neighbors_in(&EntityId::new("isolated")).unwrap();
5101        assert!(preds.is_empty());
5102    }
5103
5104    #[test]
5105    fn test_path_exists_reachable() {
5106        let g = make_graph();
5107        add(&g, "s"); add(&g, "m"); add(&g, "t");
5108        link(&g, "s", "m"); link(&g, "m", "t");
5109        assert!(g.path_exists("s", "t").unwrap());
5110    }
5111
5112    #[test]
5113    fn test_path_exists_unreachable() {
5114        let g = make_graph();
5115        add(&g, "a"); add(&g, "b");
5116        assert!(!g.path_exists("a", "b").unwrap());
5117    }
5118
5119    // ── Round 5: GraphStore::neighbor_ids ─────────────────────────────────────
5120
5121    #[test]
5122    fn test_neighbor_ids_returns_direct_successors() {
5123        let g = make_graph();
5124        add(&g, "src"); add(&g, "dst1"); add(&g, "dst2");
5125        link(&g, "src", "dst1"); link(&g, "src", "dst2");
5126        let mut ids = g.neighbor_ids(&EntityId::new("src")).unwrap();
5127        ids.sort_by_key(|id| id.0.clone());
5128        assert_eq!(ids, vec![EntityId::new("dst1"), EntityId::new("dst2")]);
5129    }
5130
5131    #[test]
5132    fn test_neighbor_ids_empty_for_isolated_node() {
5133        let g = make_graph();
5134        add(&g, "isolated");
5135        let ids = g.neighbor_ids(&EntityId::new("isolated")).unwrap();
5136        assert!(ids.is_empty());
5137    }
5138
5139    // ── Round 17: density, all_entities, all_relationships, find_entities_by_label, bfs_bounded
5140
5141    #[test]
5142    fn test_density_zero_for_empty_graph() {
5143        let g = make_graph();
5144        assert_eq!(g.density().unwrap(), 0.0);
5145    }
5146
5147    #[test]
5148    fn test_density_zero_for_single_node() {
5149        let g = make_graph();
5150        add(&g, "solo");
5151        assert_eq!(g.density().unwrap(), 0.0);
5152    }
5153
5154    #[test]
5155    fn test_density_one_for_complete_directed_graph() {
5156        // 2 nodes with both directed edges → density = 2 / (2*1) = 1.0
5157        let g = make_graph();
5158        add(&g, "a"); add(&g, "b");
5159        link(&g, "a", "b"); link(&g, "b", "a");
5160        assert!((g.density().unwrap() - 1.0).abs() < 1e-9);
5161    }
5162
5163    #[test]
5164    fn test_density_partial() {
5165        // 3 nodes, 1 edge → max edges = 6, density = 1/6
5166        let g = make_graph();
5167        add(&g, "a"); add(&g, "b"); add(&g, "c");
5168        link(&g, "a", "b");
5169        let d = g.density().unwrap();
5170        assert!((d - 1.0/6.0).abs() < 1e-9);
5171    }
5172
5173    #[test]
5174    fn test_all_entities_returns_all_nodes() {
5175        let g = make_graph();
5176        add(&g, "x"); add(&g, "y"); add(&g, "z");
5177        assert_eq!(g.all_entities().unwrap().len(), 3);
5178    }
5179
5180    #[test]
5181    fn test_all_relationships_returns_all_edges() {
5182        let g = make_graph();
5183        add(&g, "a"); add(&g, "b"); add(&g, "c");
5184        link(&g, "a", "b"); link(&g, "b", "c");
5185        assert_eq!(g.all_relationships().unwrap().len(), 2);
5186    }
5187
5188    #[test]
5189    fn test_find_entities_by_label_returns_matches() {
5190        let g = make_graph();
5191        g.add_entity(Entity::new("n1", "Person")).unwrap();
5192        g.add_entity(Entity::new("n2", "Person")).unwrap();
5193        g.add_entity(Entity::new("n3", "Car")).unwrap();
5194        let people = g.find_entities_by_label("Person").unwrap();
5195        assert_eq!(people.len(), 2);
5196    }
5197
5198    #[test]
5199    fn test_bfs_bounded_limits_depth() {
5200        // a → b → c → d, bounded at depth 2 should only reach a,b,c
5201        let g = make_graph();
5202        add(&g, "a"); add(&g, "b"); add(&g, "c"); add(&g, "d");
5203        link(&g, "a", "b"); link(&g, "b", "c"); link(&g, "c", "d");
5204        let visited = g.bfs_bounded("a", 2, 100).unwrap();
5205        assert!(visited.contains(&EntityId::new("a")));
5206        assert!(visited.contains(&EntityId::new("b")));
5207        assert!(visited.contains(&EntityId::new("c")));
5208        assert!(!visited.contains(&EntityId::new("d")));
5209    }
5210
5211    // ── Round 18: avg_degree ─────────────────────────────────────────────────
5212
5213    #[test]
5214    fn test_avg_degree_zero_for_empty_graph() {
5215        let g = make_graph();
5216        assert_eq!(g.avg_degree().unwrap(), 0.0);
5217    }
5218
5219    #[test]
5220    fn test_avg_degree_correct_value() {
5221        // 3 nodes, 2 edges → 2/3
5222        let g = make_graph();
5223        add(&g, "a"); add(&g, "b"); add(&g, "c");
5224        link(&g, "a", "b"); link(&g, "a", "c");
5225        let d = g.avg_degree().unwrap();
5226        assert!((d - 2.0/3.0).abs() < 1e-9);
5227    }
5228
5229    // ── Round 19: total_weight, max/min_edge_weight ───────────────────────────
5230
5231    #[test]
5232    fn test_total_weight_zero_for_empty_graph() {
5233        let g = make_graph();
5234        assert_eq!(g.total_weight().unwrap(), 0.0);
5235    }
5236
5237    #[test]
5238    fn test_total_weight_sums_all_edges() {
5239        let g = make_graph();
5240        add(&g, "a"); add(&g, "b"); add(&g, "c");
5241        link_w(&g, "a", "b", 2.0); link_w(&g, "b", "c", 3.5);
5242        assert!((g.total_weight().unwrap() - 5.5).abs() < 1e-6);
5243    }
5244
5245    #[test]
5246    fn test_max_edge_weight_none_for_empty() {
5247        let g = make_graph();
5248        assert!(g.max_edge_weight().unwrap().is_none());
5249    }
5250
5251    #[test]
5252    fn test_max_edge_weight_returns_largest() {
5253        let g = make_graph();
5254        add(&g, "a"); add(&g, "b"); add(&g, "c");
5255        link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
5256        assert!((g.max_edge_weight().unwrap().unwrap() - 9.5).abs() < 1e-6);
5257    }
5258
5259    #[test]
5260    fn test_min_edge_weight_returns_smallest() {
5261        let g = make_graph();
5262        add(&g, "a"); add(&g, "b"); add(&g, "c");
5263        link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
5264        assert!((g.min_edge_weight().unwrap().unwrap() - 1.0).abs() < 1e-6);
5265    }
5266
5267    // ── Round 6: max_out_degree_entity / leaf_nodes ───────────────────────────
5268
5269    #[test]
5270    fn test_max_out_degree_entity_returns_node_with_most_edges() {
5271        let g = make_graph();
5272        add(&g, "hub"); add(&g, "a"); add(&g, "b"); add(&g, "leaf");
5273        link(&g, "hub", "a"); link(&g, "hub", "b"); link(&g, "a", "leaf");
5274        let best = g.max_out_degree_entity().unwrap().unwrap();
5275        assert_eq!(best.id, EntityId::new("hub"));
5276    }
5277
5278    #[test]
5279    fn test_max_out_degree_entity_none_for_empty_graph() {
5280        let g = make_graph();
5281        assert!(g.max_out_degree_entity().unwrap().is_none());
5282    }
5283
5284    #[test]
5285    fn test_leaf_nodes_returns_nodes_with_no_outgoing_edges() {
5286        let g = make_graph();
5287        add(&g, "root"); add(&g, "mid"); add(&g, "leaf1"); add(&g, "leaf2");
5288        link(&g, "root", "mid"); link(&g, "mid", "leaf1"); link(&g, "mid", "leaf2");
5289        let mut leaf_ids: Vec<String> = g
5290            .leaf_nodes()
5291            .unwrap()
5292            .into_iter()
5293            .map(|e| e.id.0.clone())
5294            .collect();
5295        leaf_ids.sort();
5296        assert_eq!(leaf_ids, vec!["leaf1", "leaf2"]);
5297    }
5298
5299    #[test]
5300    fn test_leaf_nodes_all_are_leaves_with_no_edges() {
5301        let g = make_graph();
5302        add(&g, "a"); add(&g, "b");
5303        assert_eq!(g.leaf_nodes().unwrap().len(), 2);
5304    }
5305
5306    // ── Round 7: top_n_by_out_degree / remove_entity_and_edges ───────────────
5307
5308    #[test]
5309    fn test_top_n_by_out_degree_returns_descending() {
5310        let g = make_graph();
5311        add(&g, "hub"); add(&g, "mid"); add(&g, "tip"); add(&g, "leaf");
5312        link(&g, "hub", "mid"); link(&g, "hub", "tip"); link(&g, "hub", "leaf");
5313        link(&g, "mid", "leaf");
5314        let top2 = g.top_n_by_out_degree(2).unwrap();
5315        assert_eq!(top2.len(), 2);
5316        assert_eq!(top2[0].id, EntityId::new("hub"));
5317    }
5318
5319    #[test]
5320    fn test_top_n_by_out_degree_zero_returns_empty() {
5321        let g = make_graph();
5322        add(&g, "x");
5323        assert!(g.top_n_by_out_degree(0).unwrap().is_empty());
5324    }
5325
5326    #[test]
5327    fn test_remove_entity_and_edges_removes_node_and_incident_edges() {
5328        let g = make_graph();
5329        add(&g, "a"); add(&g, "b"); add(&g, "c");
5330        link(&g, "a", "b"); link(&g, "b", "c");
5331        g.remove_entity_and_edges(&EntityId::new("b")).unwrap();
5332        assert_eq!(g.entity_count().unwrap(), 2);
5333        assert_eq!(g.relationship_count().unwrap(), 0);
5334    }
5335
5336    #[test]
5337    fn test_remove_entity_and_edges_errors_for_unknown_id() {
5338        let g = make_graph();
5339        let result = g.remove_entity_and_edges(&EntityId::new("ghost"));
5340        assert!(result.is_err());
5341    }
5342
5343    // ── Round 20: relationship_kinds / graph_density / EntityId::try_new ──────
5344
5345    #[test]
5346    fn test_relationship_kinds_returns_sorted_distinct_kinds() {
5347        let g = make_graph();
5348        add(&g, "a"); add(&g, "b"); add(&g, "c");
5349        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5350        g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
5351        g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
5352        let kinds = g.relationship_kinds().unwrap();
5353        assert_eq!(kinds, vec!["KNOWS", "LIKES"]);
5354    }
5355
5356    #[test]
5357    fn test_relationship_kinds_empty_graph_returns_empty() {
5358        let g = make_graph();
5359        assert!(g.relationship_kinds().unwrap().is_empty());
5360    }
5361
5362    #[test]
5363    fn test_graph_density_zero_for_empty() {
5364        let g = make_graph();
5365        assert_eq!(g.graph_density().unwrap(), 0.0);
5366    }
5367
5368    #[test]
5369    fn test_graph_density_correct_for_partial_graph() {
5370        let g = make_graph();
5371        add(&g, "a"); add(&g, "b"); add(&g, "c");
5372        // 1 edge out of max 6 directed edges (3*2)
5373        link(&g, "a", "b");
5374        let d = g.graph_density().unwrap();
5375        assert!((d - 1.0 / 6.0).abs() < 1e-9);
5376    }
5377
5378    #[test]
5379    fn test_entity_id_try_new_rejects_empty() {
5380        let result = EntityId::try_new("");
5381        assert!(result.is_err());
5382    }
5383
5384    #[test]
5385    fn test_entity_id_try_new_accepts_nonempty() {
5386        let id = EntityId::try_new("valid").unwrap();
5387        assert_eq!(id.as_str(), "valid");
5388    }
5389
5390    // ── Round 8: hub_nodes / incident_relationships ───────────────────────────
5391
5392    #[test]
5393    fn test_hub_nodes_returns_nodes_meeting_threshold() {
5394        let g = make_graph();
5395        add(&g, "hub"); add(&g, "mid"); add(&g, "leaf");
5396        link(&g, "hub", "mid"); link(&g, "hub", "leaf"); link(&g, "mid", "leaf");
5397        let hubs = g.hub_nodes(2).unwrap();
5398        assert_eq!(hubs.len(), 1);
5399        assert_eq!(hubs[0].id, EntityId::new("hub"));
5400    }
5401
5402    #[test]
5403    fn test_hub_nodes_threshold_zero_returns_all() {
5404        let g = make_graph();
5405        add(&g, "a"); add(&g, "b");
5406        assert_eq!(g.hub_nodes(0).unwrap().len(), 2);
5407    }
5408
5409    #[test]
5410    fn test_incident_relationships_includes_outgoing_and_incoming() {
5411        let g = make_graph();
5412        add(&g, "a"); add(&g, "b"); add(&g, "c");
5413        link(&g, "a", "b"); link(&g, "c", "b");
5414        let rels = g.incident_relationships(&EntityId::new("b")).unwrap();
5415        assert_eq!(rels.len(), 2);
5416    }
5417
5418    #[test]
5419    fn test_incident_relationships_empty_for_isolated_node() {
5420        let g = make_graph();
5421        add(&g, "iso");
5422        assert!(g.incident_relationships(&EntityId::new("iso")).unwrap().is_empty());
5423    }
5424
5425    // ── Round 10: average_out_degree / in_degree_for ──────────────────────────
5426
5427    #[test]
5428    fn test_average_out_degree_empty_graph_is_zero() {
5429        let g = GraphStore::new();
5430        assert!((g.average_out_degree().unwrap() - 0.0).abs() < 1e-9);
5431    }
5432
5433    #[test]
5434    fn test_average_out_degree_two_nodes_one_edge() {
5435        let g = GraphStore::new();
5436        g.add_entity(Entity::new("a", "A")).unwrap();
5437        g.add_entity(Entity::new("b", "B")).unwrap();
5438        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
5439        // 1 edge / 2 nodes = 0.5
5440        assert!((g.average_out_degree().unwrap() - 0.5).abs() < 1e-9);
5441    }
5442
5443    #[test]
5444    fn test_in_degree_for_counts_incoming_edges() {
5445        let g = GraphStore::new();
5446        g.add_entity(Entity::new("a", "A")).unwrap();
5447        g.add_entity(Entity::new("b", "B")).unwrap();
5448        g.add_entity(Entity::new("c", "C")).unwrap();
5449        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5450        g.add_relationship(Relationship::new("c", "b", "r", 1.0)).unwrap();
5451        assert_eq!(g.in_degree_for(&EntityId::new("b")).unwrap(), 2);
5452        assert_eq!(g.in_degree_for(&EntityId::new("a")).unwrap(), 0);
5453    }
5454
5455    #[test]
5456    fn test_in_degree_for_returns_zero_for_unknown_entity() {
5457        let g = GraphStore::new();
5458        assert_eq!(g.in_degree_for(&EntityId::new("ghost")).unwrap(), 0);
5459    }
5460
5461    // ── Round 12: EntityId::is_empty/starts_with, Entity::has_property, entity_labels, step_latency_p50/p99 ──
5462
5463    #[test]
5464    fn test_entity_id_is_empty_false_for_nonempty() {
5465        let id = EntityId::new("node-1");
5466        assert!(!id.is_empty());
5467    }
5468
5469    #[test]
5470    fn test_entity_id_starts_with_matches_prefix() {
5471        let id = EntityId::new("concept-42");
5472        assert!(id.starts_with("concept-"));
5473        assert!(!id.starts_with("entity-"));
5474    }
5475
5476    #[test]
5477    fn test_entity_id_starts_with_empty_always_true() {
5478        let id = EntityId::new("anything");
5479        assert!(id.starts_with(""));
5480    }
5481
5482    #[test]
5483    fn test_entity_has_property_returns_true_when_present() {
5484        let e = Entity::new("e", "Node")
5485            .with_property("color", serde_json::json!("blue"));
5486        assert!(e.has_property("color"));
5487        assert!(!e.has_property("size"));
5488    }
5489
5490    #[test]
5491    fn test_entity_has_property_false_when_no_properties() {
5492        let e = Entity::new("e", "Node");
5493        assert!(!e.has_property("any"));
5494    }
5495
5496    #[test]
5497    fn test_entity_labels_returns_distinct_sorted_labels() {
5498        let g = make_graph();
5499        g.add_entity(Entity::new("a", "Person")).unwrap();
5500        g.add_entity(Entity::new("b", "Concept")).unwrap();
5501        g.add_entity(Entity::new("c", "Person")).unwrap();
5502        let labels = g.entity_labels().unwrap();
5503        assert_eq!(labels, vec!["Concept", "Person"]);
5504    }
5505
5506    #[test]
5507    fn test_entity_labels_empty_for_empty_graph() {
5508        let g = make_graph();
5509        assert!(g.entity_labels().unwrap().is_empty());
5510    }
5511
5512    // ── Round 12: out_degree_for / predecessors / is_source ───────────────────
5513
5514    #[test]
5515    fn test_out_degree_for_returns_outgoing_edge_count() {
5516        let g = GraphStore::new();
5517        g.add_entity(Entity::new("a", "A")).unwrap();
5518        g.add_entity(Entity::new("b", "B")).unwrap();
5519        g.add_entity(Entity::new("c", "C")).unwrap();
5520        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5521        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5522        assert_eq!(g.out_degree_for(&EntityId::new("a")).unwrap(), 2);
5523        assert_eq!(g.out_degree_for(&EntityId::new("b")).unwrap(), 0);
5524    }
5525
5526    #[test]
5527    fn test_out_degree_for_returns_zero_for_unknown_entity() {
5528        let g = GraphStore::new();
5529        assert_eq!(g.out_degree_for(&EntityId::new("ghost")).unwrap(), 0);
5530    }
5531
5532    #[test]
5533    fn test_predecessors_returns_nodes_with_incoming_edges() {
5534        let g = GraphStore::new();
5535        g.add_entity(Entity::new("a", "A")).unwrap();
5536        g.add_entity(Entity::new("b", "B")).unwrap();
5537        g.add_entity(Entity::new("c", "C")).unwrap();
5538        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5539        g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
5540        let mut preds: Vec<String> = g
5541            .predecessors(&EntityId::new("c"))
5542            .unwrap()
5543            .iter()
5544            .map(|e| e.id.as_str().to_string())
5545            .collect();
5546        preds.sort();
5547        assert_eq!(preds, vec!["a", "b"]);
5548    }
5549
5550    #[test]
5551    fn test_predecessors_empty_for_source_node() {
5552        let g = GraphStore::new();
5553        g.add_entity(Entity::new("root", "Root")).unwrap();
5554        g.add_entity(Entity::new("child", "Child")).unwrap();
5555        g.add_relationship(Relationship::new("root", "child", "r", 1.0)).unwrap();
5556        assert!(g.predecessors(&EntityId::new("root")).unwrap().is_empty());
5557    }
5558
5559    #[test]
5560    fn test_is_source_true_for_node_with_no_incoming_edges() {
5561        let g = GraphStore::new();
5562        g.add_entity(Entity::new("src", "Src")).unwrap();
5563        g.add_entity(Entity::new("dst", "Dst")).unwrap();
5564        g.add_relationship(Relationship::new("src", "dst", "r", 1.0)).unwrap();
5565        assert!(g.is_source(&EntityId::new("src")).unwrap());
5566        assert!(!g.is_source(&EntityId::new("dst")).unwrap());
5567    }
5568
5569    // ── Round 13: Relationship::is_self_loop/reversed, find_entities_by_labels, remove_isolated ──
5570
5571    #[test]
5572    fn test_relationship_is_self_loop_true_when_from_equals_to() {
5573        let r = Relationship::new("a", "a", "self", 1.0);
5574        assert!(r.is_self_loop());
5575    }
5576
5577    #[test]
5578    fn test_relationship_is_self_loop_false_for_normal_edge() {
5579        let r = Relationship::new("a", "b", "edge", 1.0);
5580        assert!(!r.is_self_loop());
5581    }
5582
5583    #[test]
5584    fn test_relationship_reversed_swaps_endpoints() {
5585        let r = Relationship::new("from", "to", "knows", 0.5);
5586        let rev = r.reversed();
5587        assert_eq!(rev.from.as_str(), "to");
5588        assert_eq!(rev.to.as_str(), "from");
5589        assert_eq!(rev.kind, "knows");
5590        assert!((rev.weight - 0.5).abs() < 1e-6);
5591    }
5592
5593    #[test]
5594    fn test_find_entities_by_labels_returns_matching() {
5595        let g = make_graph();
5596        g.add_entity(Entity::new("p1", "Person")).unwrap();
5597        g.add_entity(Entity::new("p2", "Person")).unwrap();
5598        g.add_entity(Entity::new("c1", "Concept")).unwrap();
5599        let results = g.find_entities_by_labels(&["Person"]).unwrap();
5600        assert_eq!(results.len(), 2);
5601        assert!(results.iter().all(|e| e.label == "Person"));
5602    }
5603
5604    #[test]
5605    fn test_find_entities_by_labels_empty_when_no_match() {
5606        let g = make_graph();
5607        g.add_entity(Entity::new("n1", "Node")).unwrap();
5608        let results = g.find_entities_by_labels(&["Missing"]).unwrap();
5609        assert!(results.is_empty());
5610    }
5611
5612    #[test]
5613    fn test_remove_isolated_removes_nodes_without_edges() {
5614        let g = make_graph();
5615        g.add_entity(Entity::new("connected", "N")).unwrap();
5616        g.add_entity(Entity::new("isolated", "N")).unwrap();
5617        g.add_entity(Entity::new("other", "N")).unwrap();
5618        g.add_relationship(Relationship::new("connected", "other", "r", 1.0)).unwrap();
5619        let removed = g.remove_isolated().unwrap();
5620        assert_eq!(removed, 1);
5621        assert!(g.get_entity(&EntityId::new("isolated")).is_err());
5622        assert!(g.get_entity(&EntityId::new("connected")).is_ok());
5623    }
5624
5625    #[test]
5626    fn test_remove_isolated_zero_when_all_connected() {
5627        let g = make_graph();
5628        g.add_entity(Entity::new("a", "N")).unwrap();
5629        g.add_entity(Entity::new("b", "N")).unwrap();
5630        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5631        assert_eq!(g.remove_isolated().unwrap(), 0);
5632    }
5633
5634    // ── Round 13: successors / is_sink ────────────────────────────────────────
5635
5636    #[test]
5637    fn test_successors_returns_direct_out_neighbors() {
5638        let g = GraphStore::new();
5639        g.add_entity(Entity::new("a", "A")).unwrap();
5640        g.add_entity(Entity::new("b", "B")).unwrap();
5641        g.add_entity(Entity::new("c", "C")).unwrap();
5642        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5643        g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5644        let mut ids: Vec<String> = g
5645            .successors(&EntityId::new("a"))
5646            .unwrap()
5647            .iter()
5648            .map(|e| e.id.as_str().to_string())
5649            .collect();
5650        ids.sort();
5651        assert_eq!(ids, vec!["b", "c"]);
5652    }
5653
5654    #[test]
5655    fn test_successors_empty_for_sink_node() {
5656        let g = GraphStore::new();
5657        g.add_entity(Entity::new("leaf", "L")).unwrap();
5658        assert!(g.successors(&EntityId::new("leaf")).unwrap().is_empty());
5659    }
5660
5661    #[test]
5662    fn test_is_sink_true_for_node_with_no_outgoing_edges() {
5663        let g = GraphStore::new();
5664        g.add_entity(Entity::new("a", "A")).unwrap();
5665        g.add_entity(Entity::new("b", "B")).unwrap();
5666        g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5667        assert!(!g.is_sink(&EntityId::new("a")).unwrap());
5668        assert!(g.is_sink(&EntityId::new("b")).unwrap());
5669    }
5670
5671    #[test]
5672    fn test_is_sink_true_for_unknown_entity() {
5673        let g = GraphStore::new();
5674        assert!(g.is_sink(&EntityId::new("ghost")).unwrap());
5675    }
5676
5677    // ── Round 14: reachable_from / contains_cycle ─────────────────────────────
5678
5679    #[test]
5680    fn test_reachable_from_returns_all_downstream_nodes() {
5681        let g = GraphStore::new();
5682        g.add_entity(Entity::new("a", "N")).unwrap();
5683        g.add_entity(Entity::new("b", "N")).unwrap();
5684        g.add_entity(Entity::new("c", "N")).unwrap();
5685        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
5686        g.add_relationship(Relationship::new("b", "c", "edge", 1.0)).unwrap();
5687        let reachable = g.reachable_from(&EntityId::new("a")).unwrap();
5688        assert!(reachable.contains(&EntityId::new("b")));
5689        assert!(reachable.contains(&EntityId::new("c")));
5690        assert!(!reachable.contains(&EntityId::new("a")));
5691    }
5692
5693    #[test]
5694    fn test_reachable_from_empty_for_sink_node() {
5695        let g = GraphStore::new();
5696        g.add_entity(Entity::new("sink", "N")).unwrap();
5697        let reachable = g.reachable_from(&EntityId::new("sink")).unwrap();
5698        assert!(reachable.is_empty());
5699    }
5700
5701    #[test]
5702    fn test_reachable_from_empty_for_unknown_node() {
5703        let g = GraphStore::new();
5704        let reachable = g.reachable_from(&EntityId::new("ghost")).unwrap();
5705        assert!(reachable.is_empty());
5706    }
5707
5708    #[test]
5709    fn test_contains_cycle_false_for_dag() {
5710        let g = GraphStore::new();
5711        g.add_entity(Entity::new("a", "N")).unwrap();
5712        g.add_entity(Entity::new("b", "N")).unwrap();
5713        g.add_entity(Entity::new("c", "N")).unwrap();
5714        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5715        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5716        assert!(!g.contains_cycle().unwrap());
5717    }
5718
5719    #[test]
5720    fn test_contains_cycle_true_for_cyclic_graph() {
5721        let g = GraphStore::new();
5722        g.add_entity(Entity::new("x", "N")).unwrap();
5723        g.add_entity(Entity::new("y", "N")).unwrap();
5724        g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
5725        g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
5726        assert!(g.contains_cycle().unwrap());
5727    }
5728
5729    #[test]
5730    fn test_contains_cycle_false_for_empty_graph() {
5731        let g = GraphStore::new();
5732        assert!(!g.contains_cycle().unwrap());
5733    }
5734
5735    // ── Round 15: GraphStore::is_acyclic ─────────────────────────────────────
5736
5737    #[test]
5738    fn test_is_acyclic_true_for_dag() {
5739        let g = GraphStore::new();
5740        g.add_entity(Entity::new("a", "N")).unwrap();
5741        g.add_entity(Entity::new("b", "N")).unwrap();
5742        g.add_entity(Entity::new("c", "N")).unwrap();
5743        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5744        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5745        assert!(g.is_acyclic().unwrap());
5746    }
5747
5748    #[test]
5749    fn test_is_acyclic_false_for_cyclic_graph() {
5750        let g = GraphStore::new();
5751        g.add_entity(Entity::new("x", "N")).unwrap();
5752        g.add_entity(Entity::new("y", "N")).unwrap();
5753        g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
5754        g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
5755        assert!(!g.is_acyclic().unwrap());
5756    }
5757
5758    #[test]
5759    fn test_is_acyclic_true_for_empty_graph() {
5760        let g = GraphStore::new();
5761        assert!(g.is_acyclic().unwrap());
5762    }
5763
5764    // ── Round 15: count_relationships_by_kind, merge, top_nodes_by_in/out_degree ──
5765
5766    #[test]
5767    fn test_count_relationships_by_kind_returns_correct_count() {
5768        let g = make_graph();
5769        g.add_entity(Entity::new("a", "N")).unwrap();
5770        g.add_entity(Entity::new("b", "N")).unwrap();
5771        g.add_entity(Entity::new("c", "N")).unwrap();
5772        g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
5773        g.add_relationship(Relationship::new("b", "c", "knows", 1.0)).unwrap();
5774        g.add_relationship(Relationship::new("a", "c", "likes", 0.5)).unwrap();
5775        assert_eq!(g.count_relationships_by_kind("knows").unwrap(), 2);
5776        assert_eq!(g.count_relationships_by_kind("likes").unwrap(), 1);
5777        assert_eq!(g.count_relationships_by_kind("absent").unwrap(), 0);
5778    }
5779
5780    #[test]
5781    fn test_merge_imports_entities_and_relationships() {
5782        let g1 = make_graph();
5783        g1.add_entity(Entity::new("a", "N")).unwrap();
5784        g1.add_entity(Entity::new("b", "N")).unwrap();
5785        g1.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5786
5787        let g2 = make_graph();
5788        g2.add_entity(Entity::new("c", "N")).unwrap();
5789        g2.add_entity(Entity::new("a", "N")).unwrap(); // duplicate — should not double-add
5790        g2.add_relationship(Relationship::new("c", "a", "s", 0.5)).unwrap();
5791
5792        g1.merge(&g2).unwrap();
5793        assert_eq!(g1.entity_count().unwrap(), 3); // a, b, c
5794        assert_eq!(g1.relationship_count().unwrap(), 2); // r + s
5795    }
5796
5797    #[test]
5798    fn test_top_nodes_by_in_degree_returns_sinks() {
5799        let g = make_graph();
5800        g.add_entity(Entity::new("hub", "N")).unwrap();
5801        g.add_entity(Entity::new("src1", "N")).unwrap();
5802        g.add_entity(Entity::new("src2", "N")).unwrap();
5803        g.add_relationship(Relationship::new("src1", "hub", "r", 1.0)).unwrap();
5804        g.add_relationship(Relationship::new("src2", "hub", "r", 1.0)).unwrap();
5805        let top = g.top_nodes_by_in_degree(1).unwrap();
5806        assert_eq!(top.len(), 1);
5807        assert_eq!(top[0].id.as_str(), "hub");
5808    }
5809
5810    #[test]
5811    fn test_top_nodes_by_out_degree_returns_sources() {
5812        let g = make_graph();
5813        g.add_entity(Entity::new("src", "N")).unwrap();
5814        g.add_entity(Entity::new("a", "N")).unwrap();
5815        g.add_entity(Entity::new("b", "N")).unwrap();
5816        g.add_relationship(Relationship::new("src", "a", "r", 1.0)).unwrap();
5817        g.add_relationship(Relationship::new("src", "b", "r", 1.0)).unwrap();
5818        let top = g.top_nodes_by_out_degree(1).unwrap();
5819        assert_eq!(top.len(), 1);
5820        assert_eq!(top[0].id.as_str(), "src");
5821    }
5822
5823    // ── Round 27: property_value, find_relationships_by_kind, count_relationships_by_kind ──
5824
5825    #[test]
5826    fn test_entity_property_value_returns_value() {
5827        let e = Entity::new("n1", "Node")
5828            .with_property("age", serde_json::Value::Number(42.into()));
5829        let val = e.property_value("age");
5830        assert!(val.is_some());
5831        assert_eq!(val.unwrap(), &serde_json::Value::Number(42.into()));
5832    }
5833
5834    #[test]
5835    fn test_entity_property_value_missing_returns_none() {
5836        let e = Entity::new("n1", "Node");
5837        assert!(e.property_value("missing").is_none());
5838    }
5839
5840    #[test]
5841    fn test_find_relationships_by_kind_returns_matching() {
5842        let g = GraphStore::new();
5843        g.add_entity(Entity::new("a", "N")).unwrap();
5844        g.add_entity(Entity::new("b", "N")).unwrap();
5845        g.add_entity(Entity::new("c", "N")).unwrap();
5846        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5847        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
5848        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
5849        let rels = g.find_relationships_by_kind("KNOWS").unwrap();
5850        assert_eq!(rels.len(), 2);
5851        assert!(rels.iter().all(|r| r.kind == "KNOWS"));
5852    }
5853
5854    #[test]
5855    fn test_find_relationships_by_kind_no_match_returns_empty() {
5856        let g = GraphStore::new();
5857        g.add_entity(Entity::new("a", "N")).unwrap();
5858        g.add_entity(Entity::new("b", "N")).unwrap();
5859        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5860        let rels = g.find_relationships_by_kind("HATES").unwrap();
5861        assert!(rels.is_empty());
5862    }
5863
5864    #[test]
5865    fn test_count_relationships_by_kind_correct() {
5866        let g = GraphStore::new();
5867        g.add_entity(Entity::new("a", "N")).unwrap();
5868        g.add_entity(Entity::new("b", "N")).unwrap();
5869        g.add_entity(Entity::new("c", "N")).unwrap();
5870        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5871        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
5872        g.add_relationship(Relationship::new("a", "c", "PART_OF", 1.0)).unwrap();
5873        assert_eq!(g.count_relationships_by_kind("KNOWS").unwrap(), 2);
5874        assert_eq!(g.count_relationships_by_kind("PART_OF").unwrap(), 1);
5875        assert_eq!(g.count_relationships_by_kind("MISSING").unwrap(), 0);
5876    }
5877
5878    // ── Round 16: max_out_degree / max_in_degree ──────────────────────────────
5879
5880    #[test]
5881    fn test_max_out_degree_returns_highest_out_degree() {
5882        let g = GraphStore::new();
5883        g.add_entity(Entity::new("a", "N")).unwrap();
5884        g.add_entity(Entity::new("b", "N")).unwrap();
5885        g.add_entity(Entity::new("c", "N")).unwrap();
5886        // a → b, a → c  (out-degree 2)
5887        // b → c          (out-degree 1)
5888        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5889        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
5890        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5891        assert_eq!(g.max_out_degree().unwrap(), 2);
5892    }
5893
5894    #[test]
5895    fn test_max_out_degree_zero_for_empty_graph() {
5896        let g = GraphStore::new();
5897        assert_eq!(g.max_out_degree().unwrap(), 0);
5898    }
5899
5900    #[test]
5901    fn test_max_in_degree_returns_highest_in_degree() {
5902        let g = GraphStore::new();
5903        g.add_entity(Entity::new("a", "N")).unwrap();
5904        g.add_entity(Entity::new("b", "N")).unwrap();
5905        g.add_entity(Entity::new("c", "N")).unwrap();
5906        // a → c, b → c  (c has in-degree 2)
5907        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
5908        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5909        assert_eq!(g.max_in_degree().unwrap(), 2);
5910    }
5911
5912    #[test]
5913    fn test_max_in_degree_zero_for_empty_graph() {
5914        let g = GraphStore::new();
5915        assert_eq!(g.max_in_degree().unwrap(), 0);
5916    }
5917
5918    // ── Round 17: sum_edge_weights ────────────────────────────────────────────
5919
5920    #[test]
5921    fn test_sum_edge_weights_correct_sum() {
5922        let g = GraphStore::new();
5923        g.add_entity(Entity::new("a", "N")).unwrap();
5924        g.add_entity(Entity::new("b", "N")).unwrap();
5925        g.add_entity(Entity::new("c", "N")).unwrap();
5926        g.add_relationship(Relationship::new("a", "b", "e", 1.5)).unwrap();
5927        g.add_relationship(Relationship::new("b", "c", "e", 2.5)).unwrap();
5928        let total = g.sum_edge_weights().unwrap();
5929        assert!((total - 4.0).abs() < 1e-9);
5930    }
5931
5932    #[test]
5933    fn test_sum_edge_weights_zero_for_empty_graph() {
5934        let g = GraphStore::new();
5935        assert!((g.sum_edge_weights().unwrap() - 0.0).abs() < 1e-9);
5936    }
5937
5938    // ── Round 22: weight_stats ────────────────────────────────────────────────
5939
5940    #[test]
5941    fn test_weight_stats_none_for_empty_graph() {
5942        let g = GraphStore::new();
5943        assert!(g.weight_stats().unwrap().is_none());
5944    }
5945
5946    #[test]
5947    fn test_weight_stats_returns_correct_min_max_mean() {
5948        let g = GraphStore::new();
5949        g.add_entity(Entity::new("a", "N")).unwrap();
5950        g.add_entity(Entity::new("b", "N")).unwrap();
5951        g.add_entity(Entity::new("c", "N")).unwrap();
5952        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5953        g.add_relationship(Relationship::new("b", "c", "e", 3.0)).unwrap();
5954        let (min, max, mean) = g.weight_stats().unwrap().unwrap();
5955        assert!((min - 1.0).abs() < 1e-9);
5956        assert!((max - 3.0).abs() < 1e-9);
5957        assert!((mean - 2.0).abs() < 1e-9);
5958    }
5959
5960    // ── Round 23: GraphStore::isolated_nodes ─────────────────────────────────
5961
5962    #[test]
5963    fn test_isolated_nodes_empty_graph_returns_empty_set() {
5964        let g = GraphStore::new();
5965        assert!(g.isolated_nodes().unwrap().is_empty());
5966    }
5967
5968    #[test]
5969    fn test_isolated_nodes_all_connected_returns_empty() {
5970        let g = GraphStore::new();
5971        g.add_entity(Entity::new("a", "N")).unwrap();
5972        g.add_entity(Entity::new("b", "N")).unwrap();
5973        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5974        // both a (out) and b (in) have an edge
5975        assert!(g.isolated_nodes().unwrap().is_empty());
5976    }
5977
5978    #[test]
5979    fn test_isolated_nodes_returns_orphan_entity() {
5980        let g = GraphStore::new();
5981        g.add_entity(Entity::new("a", "N")).unwrap();
5982        g.add_entity(Entity::new("b", "N")).unwrap();
5983        g.add_entity(Entity::new("orphan", "N")).unwrap();
5984        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5985        let iso = g.isolated_nodes().unwrap();
5986        assert_eq!(iso.len(), 1);
5987        assert!(iso.contains(&EntityId::new("orphan")));
5988    }
5989
5990    // ── Round 30: reverse, max_in_degree_entity, shortest_path_length ────────
5991
5992    #[test]
5993    fn test_reverse_flips_edge_direction() {
5994        let g = GraphStore::new();
5995        g.add_entity(Entity::new("x", "N")).unwrap();
5996        g.add_entity(Entity::new("y", "N")).unwrap();
5997        g.add_relationship(Relationship::new("x", "y", "edge", 1.0)).unwrap();
5998        let rev = g.reverse().unwrap();
5999        // In the reversed graph, y → x should exist
6000        let y_id = EntityId::new("y");
6001        let succs = rev.successors(&y_id).unwrap();
6002        assert!(succs.iter().any(|e| e.id == EntityId::new("x")));
6003    }
6004
6005    #[test]
6006    fn test_reverse_empty_graph_produces_empty_reverse() {
6007        let g = GraphStore::new();
6008        let rev = g.reverse().unwrap();
6009        assert!(rev.is_empty().unwrap());
6010    }
6011
6012    #[test]
6013    fn test_max_in_degree_entity_none_for_empty_graph() {
6014        let g = GraphStore::new();
6015        assert!(g.max_in_degree_entity().unwrap().is_none());
6016    }
6017
6018    #[test]
6019    fn test_max_in_degree_entity_returns_node_with_most_incoming() {
6020        let g = GraphStore::new();
6021        g.add_entity(Entity::new("hub", "N")).unwrap();
6022        g.add_entity(Entity::new("a", "N")).unwrap();
6023        g.add_entity(Entity::new("b", "N")).unwrap();
6024        g.add_relationship(Relationship::new("a", "hub", "e", 1.0)).unwrap();
6025        g.add_relationship(Relationship::new("b", "hub", "e", 1.0)).unwrap();
6026        let best = g.max_in_degree_entity().unwrap().unwrap();
6027        assert_eq!(best.id, EntityId::new("hub"));
6028    }
6029
6030    #[test]
6031    fn test_shortest_path_length_none_when_no_path() {
6032        let g = GraphStore::new();
6033        g.add_entity(Entity::new("a", "N")).unwrap();
6034        g.add_entity(Entity::new("b", "N")).unwrap();
6035        let len = g
6036            .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
6037            .unwrap();
6038        assert!(len.is_none());
6039    }
6040
6041    #[test]
6042    fn test_shortest_path_length_one_for_direct_edge() {
6043        let g = GraphStore::new();
6044        g.add_entity(Entity::new("a", "N")).unwrap();
6045        g.add_entity(Entity::new("b", "N")).unwrap();
6046        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6047        let len = g
6048            .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
6049            .unwrap();
6050        assert_eq!(len, Some(1));
6051    }
6052
6053    #[test]
6054    fn test_shortest_path_length_two_for_two_hop_path() {
6055        let g = GraphStore::new();
6056        g.add_entity(Entity::new("a", "N")).unwrap();
6057        g.add_entity(Entity::new("b", "N")).unwrap();
6058        g.add_entity(Entity::new("c", "N")).unwrap();
6059        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6060        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
6061        let len = g
6062            .shortest_path_length(&EntityId::new("a"), &EntityId::new("c"))
6063            .unwrap();
6064        assert_eq!(len, Some(2));
6065    }
6066
6067    // ── Round 25: avg_out_degree / avg_in_degree ──────────────────────────────
6068
6069    #[test]
6070    fn test_avg_out_degree_zero_for_empty_graph() {
6071        let g = GraphStore::new();
6072        assert!((g.avg_out_degree().unwrap() - 0.0).abs() < 1e-9);
6073    }
6074
6075    #[test]
6076    fn test_avg_out_degree_correct_mean() {
6077        let g = GraphStore::new();
6078        g.add_entity(Entity::new("a", "N")).unwrap();
6079        g.add_entity(Entity::new("b", "N")).unwrap();
6080        g.add_entity(Entity::new("c", "N")).unwrap();
6081        // a → b, a → c (a has out-degree 2), b and c have 0
6082        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6083        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
6084        // mean = (2 + 0 + 0) / 3 ≈ 0.667
6085        let avg = g.avg_out_degree().unwrap();
6086        assert!((avg - 2.0 / 3.0).abs() < 1e-9);
6087    }
6088
6089    #[test]
6090    fn test_avg_in_degree_zero_for_empty_graph() {
6091        let g = GraphStore::new();
6092        assert!((g.avg_in_degree().unwrap() - 0.0).abs() < 1e-9);
6093    }
6094
6095    #[test]
6096    fn test_avg_in_degree_correct_mean() {
6097        let g = GraphStore::new();
6098        g.add_entity(Entity::new("a", "N")).unwrap();
6099        g.add_entity(Entity::new("b", "N")).unwrap();
6100        g.add_entity(Entity::new("c", "N")).unwrap();
6101        // a → c, b → c (c has in-degree 2), a and b have 0
6102        g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
6103        g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
6104        // mean = (0 + 0 + 2) / 3 ≈ 0.667
6105        let avg = g.avg_in_degree().unwrap();
6106        assert!((avg - 2.0 / 3.0).abs() < 1e-9);
6107    }
6108
6109    // ── Round 26: has_entity ──────────────────────────────────────────────────
6110
6111    #[test]
6112    fn test_graph_store_has_entity_false_when_missing() {
6113        let g = GraphStore::new();
6114        let id = EntityId::new("nonexistent");
6115        assert!(!g.has_entity(&id).unwrap());
6116    }
6117
6118    #[test]
6119    fn test_graph_store_has_entity_true_after_add() {
6120        let g = GraphStore::new();
6121        g.add_entity(Entity::new("node-a", "Person")).unwrap();
6122        let id = EntityId::new("node-a");
6123        assert!(g.has_entity(&id).unwrap());
6124    }
6125
6126    // ── Round 32: Entity::with_property, GraphStore::remove_relationship,
6127    //             GraphStore::update_entity_property ─────────────────────────
6128
6129    #[test]
6130    fn test_entity_with_property_stores_value() {
6131        let e = Entity::new("e1", "Label")
6132            .with_property("score", serde_json::json!(42));
6133        assert_eq!(e.property_value("score"), Some(&serde_json::json!(42)));
6134    }
6135
6136    #[test]
6137    fn test_entity_with_property_chaining() {
6138        let e = Entity::new("e2", "L")
6139            .with_property("a", serde_json::json!(1))
6140            .with_property("b", serde_json::json!(2));
6141        assert!(e.has_property("a"));
6142        assert!(e.has_property("b"));
6143    }
6144
6145    #[test]
6146    fn test_remove_relationship_succeeds_when_exists() {
6147        let g = GraphStore::new();
6148        g.add_entity(Entity::new("x", "N")).unwrap();
6149        g.add_entity(Entity::new("y", "N")).unwrap();
6150        g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
6151        g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "link").unwrap();
6152        assert_eq!(g.relationship_count().unwrap(), 0);
6153    }
6154
6155    #[test]
6156    fn test_remove_relationship_errors_when_missing() {
6157        let g = GraphStore::new();
6158        g.add_entity(Entity::new("x", "N")).unwrap();
6159        g.add_entity(Entity::new("y", "N")).unwrap();
6160        let result = g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "ghost");
6161        assert!(result.is_err());
6162    }
6163
6164    #[test]
6165    fn test_update_entity_property_returns_true_when_entity_exists() {
6166        let g = GraphStore::new();
6167        g.add_entity(Entity::new("node", "N")).unwrap();
6168        let updated = g
6169            .update_entity_property(&EntityId::new("node"), "color", serde_json::json!("red"))
6170            .unwrap();
6171        assert!(updated);
6172        let entity = g.get_entity(&EntityId::new("node")).unwrap();
6173        assert_eq!(entity.property_value("color"), Some(&serde_json::json!("red")));
6174    }
6175
6176    #[test]
6177    fn test_update_entity_property_returns_false_for_unknown_entity() {
6178        let g = GraphStore::new();
6179        let updated = g
6180            .update_entity_property(&EntityId::new("ghost"), "key", serde_json::json!(1))
6181            .unwrap();
6182        assert!(!updated);
6183    }
6184
6185    // ── Round 27: has_any_entities ────────────────────────────────────────────
6186
6187    #[test]
6188    fn test_graph_store_has_any_entities_false_when_empty() {
6189        let g = GraphStore::new();
6190        assert!(!g.has_any_entities().unwrap());
6191    }
6192
6193    #[test]
6194    fn test_graph_store_has_any_entities_true_after_add() {
6195        let g = GraphStore::new();
6196        g.add_entity(Entity::new("x", "Node")).unwrap();
6197        assert!(g.has_any_entities().unwrap());
6198    }
6199
6200    // ── Round 28: entity_type_count ───────────────────────────────────────────
6201
6202    #[test]
6203    fn test_entity_type_count_zero_for_empty_graph() {
6204        let g = GraphStore::new();
6205        assert_eq!(g.entity_type_count().unwrap(), 0);
6206    }
6207
6208    #[test]
6209    fn test_entity_type_count_counts_distinct_labels() {
6210        let g = GraphStore::new();
6211        g.add_entity(Entity::new("a", "Person")).unwrap();
6212        g.add_entity(Entity::new("b", "Person")).unwrap();
6213        g.add_entity(Entity::new("c", "Concept")).unwrap();
6214        // "Person" and "Concept" → 2 distinct types
6215        assert_eq!(g.entity_type_count().unwrap(), 2);
6216    }
6217
6218    // ── Round 29: orphan_count ────────────────────────────────────────────────
6219
6220    #[test]
6221    fn test_orphan_count_zero_for_empty_graph() {
6222        let g = GraphStore::new();
6223        assert_eq!(g.orphan_count().unwrap(), 0);
6224    }
6225
6226    #[test]
6227    fn test_orphan_count_all_orphans_with_no_relationships() {
6228        let g = GraphStore::new();
6229        g.add_entity(Entity::new("a", "N")).unwrap();
6230        g.add_entity(Entity::new("b", "N")).unwrap();
6231        assert_eq!(g.orphan_count().unwrap(), 2);
6232    }
6233
6234    #[test]
6235    fn test_orphan_count_excludes_entities_with_edges() {
6236        let g = GraphStore::new();
6237        g.add_entity(Entity::new("a", "N")).unwrap();
6238        g.add_entity(Entity::new("b", "N")).unwrap();
6239        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
6240        // "a" has out-edge → not orphan; "b" has no out-edges → orphan
6241        assert_eq!(g.orphan_count().unwrap(), 1);
6242    }
6243
6244    // ── Round 30: labels ──────────────────────────────────────────────────────
6245
6246    #[test]
6247    fn test_labels_empty_for_empty_graph() {
6248        let g = GraphStore::new();
6249        assert!(g.labels().unwrap().is_empty());
6250    }
6251
6252    #[test]
6253    fn test_labels_returns_distinct_sorted_labels() {
6254        let g = GraphStore::new();
6255        g.add_entity(Entity::new("a", "Concept")).unwrap();
6256        g.add_entity(Entity::new("b", "Person")).unwrap();
6257        g.add_entity(Entity::new("c", "Concept")).unwrap();
6258        assert_eq!(g.labels().unwrap(), vec!["Concept".to_string(), "Person".to_string()]);
6259    }
6260
6261    #[test]
6262    fn test_incoming_count_for_counts_inbound_edges() {
6263        let g = GraphStore::new();
6264        g.add_entity(Entity::new("a", "Node")).unwrap();
6265        g.add_entity(Entity::new("b", "Node")).unwrap();
6266        g.add_entity(Entity::new("c", "Node")).unwrap();
6267        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6268        g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
6269        assert_eq!(g.incoming_count_for(&EntityId::new("b")).unwrap(), 2);
6270        assert_eq!(g.incoming_count_for(&EntityId::new("a")).unwrap(), 0);
6271    }
6272
6273    #[test]
6274    fn test_outgoing_count_for_counts_outbound_edges() {
6275        let g = GraphStore::new();
6276        g.add_entity(Entity::new("a", "Node")).unwrap();
6277        g.add_entity(Entity::new("b", "Node")).unwrap();
6278        g.add_entity(Entity::new("c", "Node")).unwrap();
6279        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6280        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6281        assert_eq!(g.outgoing_count_for(&EntityId::new("a")).unwrap(), 2);
6282        assert_eq!(g.outgoing_count_for(&EntityId::new("b")).unwrap(), 0);
6283    }
6284
6285    #[test]
6286    fn test_source_count_returns_number_of_nodes_with_no_incoming_edges() {
6287        let g = GraphStore::new();
6288        g.add_entity(Entity::new("a", "Node")).unwrap();
6289        g.add_entity(Entity::new("b", "Node")).unwrap();
6290        g.add_entity(Entity::new("c", "Node")).unwrap();
6291        // a→b, a→c: b and c have incoming edges; a has none
6292        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6293        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6294        assert_eq!(g.source_count().unwrap(), 1);
6295    }
6296
6297    #[test]
6298    fn test_source_count_all_isolated_nodes_are_sources() {
6299        let g = GraphStore::new();
6300        g.add_entity(Entity::new("x", "Node")).unwrap();
6301        g.add_entity(Entity::new("y", "Node")).unwrap();
6302        assert_eq!(g.source_count().unwrap(), 2);
6303    }
6304
6305    #[test]
6306    fn test_sink_count_returns_nodes_with_no_outgoing_edges() {
6307        let g = GraphStore::new();
6308        g.add_entity(Entity::new("a", "Node")).unwrap();
6309        g.add_entity(Entity::new("b", "Node")).unwrap();
6310        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6311        // b has no outgoing edges → sink
6312        assert_eq!(g.sink_count().unwrap(), 1);
6313        assert_eq!(g.source_count().unwrap(), 1);
6314    }
6315
6316    #[test]
6317    fn test_has_self_loops_true_when_self_loop_exists() {
6318        let g = GraphStore::new();
6319        g.add_entity(Entity::new("a", "Node")).unwrap();
6320        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
6321        assert!(g.has_self_loops().unwrap());
6322    }
6323
6324    #[test]
6325    fn test_has_self_loops_false_when_no_self_loops() {
6326        let g = GraphStore::new();
6327        g.add_entity(Entity::new("a", "Node")).unwrap();
6328        g.add_entity(Entity::new("b", "Node")).unwrap();
6329        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6330        assert!(!g.has_self_loops().unwrap());
6331    }
6332
6333    #[test]
6334    fn test_bidirectional_count_counts_mutual_edges() {
6335        let g = GraphStore::new();
6336        g.add_entity(Entity::new("a", "Node")).unwrap();
6337        g.add_entity(Entity::new("b", "Node")).unwrap();
6338        g.add_entity(Entity::new("c", "Node")).unwrap();
6339        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6340        g.add_relationship(Relationship::new("b", "a", "link", 1.0)).unwrap(); // bidirectional pair
6341        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap(); // one-way
6342        assert_eq!(g.bidirectional_count().unwrap(), 1);
6343    }
6344
6345    #[test]
6346    fn test_bidirectional_count_zero_when_no_mutual_edges() {
6347        let g = GraphStore::new();
6348        g.add_entity(Entity::new("a", "Node")).unwrap();
6349        g.add_entity(Entity::new("b", "Node")).unwrap();
6350        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6351        assert_eq!(g.bidirectional_count().unwrap(), 0);
6352    }
6353
6354    // ── Round 36 ──────────────────────────────────────────────────────────────
6355
6356    #[test]
6357    fn test_relationship_kind_count_counts_distinct_kinds() {
6358        let g = GraphStore::new();
6359        g.add_entity(Entity::new("a", "N")).unwrap();
6360        g.add_entity(Entity::new("b", "N")).unwrap();
6361        g.add_entity(Entity::new("c", "N")).unwrap();
6362        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6363        g.add_relationship(Relationship::new("b", "c", "friend", 1.0)).unwrap();
6364        g.add_relationship(Relationship::new("a", "c", "enemy", 1.0)).unwrap();
6365        assert_eq!(g.relationship_kind_count().unwrap(), 2);
6366    }
6367
6368    #[test]
6369    fn test_relationship_kind_count_zero_when_empty() {
6370        let g = GraphStore::new();
6371        assert_eq!(g.relationship_kind_count().unwrap(), 0);
6372    }
6373
6374    #[test]
6375    fn test_entities_with_self_loops_returns_self_loop_ids() {
6376        let g = GraphStore::new();
6377        g.add_entity(Entity::new("a", "N")).unwrap();
6378        g.add_entity(Entity::new("b", "N")).unwrap();
6379        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
6380        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6381        let ids = g.entities_with_self_loops().unwrap();
6382        assert_eq!(ids, vec![EntityId::new("a")]);
6383    }
6384
6385    #[test]
6386    fn test_entities_with_self_loops_empty_when_no_self_loops() {
6387        let g = GraphStore::new();
6388        g.add_entity(Entity::new("a", "N")).unwrap();
6389        g.add_entity(Entity::new("b", "N")).unwrap();
6390        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6391        assert!(g.entities_with_self_loops().unwrap().is_empty());
6392    }
6393
6394    // ── Round 37 ──────────────────────────────────────────────────────────────
6395
6396    #[test]
6397    fn test_isolated_entity_count_returns_count_with_no_edges() {
6398        let g = GraphStore::new();
6399        g.add_entity(Entity::new("a", "N")).unwrap();
6400        g.add_entity(Entity::new("b", "N")).unwrap();
6401        g.add_entity(Entity::new("c", "N")).unwrap();
6402        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6403        // c is isolated (no incoming, no outgoing)
6404        assert_eq!(g.isolated_entity_count().unwrap(), 1);
6405    }
6406
6407    #[test]
6408    fn test_isolated_entity_count_zero_when_all_connected() {
6409        let g = GraphStore::new();
6410        g.add_entity(Entity::new("a", "N")).unwrap();
6411        g.add_entity(Entity::new("b", "N")).unwrap();
6412        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6413        assert_eq!(g.isolated_entity_count().unwrap(), 0);
6414    }
6415
6416    #[test]
6417    fn test_avg_relationship_weight_returns_mean() {
6418        let g = GraphStore::new();
6419        g.add_entity(Entity::new("a", "N")).unwrap();
6420        g.add_entity(Entity::new("b", "N")).unwrap();
6421        g.add_entity(Entity::new("c", "N")).unwrap();
6422        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6423        g.add_relationship(Relationship::new("b", "c", "link", 3.0)).unwrap();
6424        let avg = g.avg_relationship_weight().unwrap();
6425        assert!((avg - 2.0).abs() < 1e-6);
6426    }
6427
6428    #[test]
6429    fn test_avg_relationship_weight_zero_when_no_relationships() {
6430        let g = GraphStore::new();
6431        assert_eq!(g.avg_relationship_weight().unwrap(), 0.0);
6432    }
6433
6434    // ── Round 38 ──────────────────────────────────────────────────────────────
6435
6436    #[test]
6437    fn test_total_in_degree_equals_relationship_count() {
6438        let g = GraphStore::new();
6439        g.add_entity(Entity::new("a", "N")).unwrap();
6440        g.add_entity(Entity::new("b", "N")).unwrap();
6441        g.add_entity(Entity::new("c", "N")).unwrap();
6442        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6443        g.add_relationship(Relationship::new("b", "c", "link", 1.0)).unwrap();
6444        assert_eq!(g.total_in_degree().unwrap(), 2);
6445    }
6446
6447    #[test]
6448    fn test_total_in_degree_zero_when_no_relationships() {
6449        let g = GraphStore::new();
6450        assert_eq!(g.total_in_degree().unwrap(), 0);
6451    }
6452
6453    #[test]
6454    fn test_relationship_count_between_counts_edges_between_pair() {
6455        let g = GraphStore::new();
6456        g.add_entity(Entity::new("a", "N")).unwrap();
6457        g.add_entity(Entity::new("b", "N")).unwrap();
6458        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6459        g.add_relationship(Relationship::new("a", "b", "colleague", 1.0)).unwrap();
6460        let from = EntityId::new("a");
6461        let to = EntityId::new("b");
6462        assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 2);
6463    }
6464
6465    #[test]
6466    fn test_relationship_count_between_returns_zero_for_no_edge() {
6467        let g = GraphStore::new();
6468        g.add_entity(Entity::new("a", "N")).unwrap();
6469        g.add_entity(Entity::new("b", "N")).unwrap();
6470        let from = EntityId::new("a");
6471        let to = EntityId::new("b");
6472        assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 0);
6473    }
6474
6475    // ── Round 39 ──────────────────────────────────────────────────────────────
6476
6477    #[test]
6478    fn test_edges_from_returns_all_outgoing_relationships() {
6479        let g = GraphStore::new();
6480        g.add_entity(Entity::new("a", "N")).unwrap();
6481        g.add_entity(Entity::new("b", "N")).unwrap();
6482        g.add_entity(Entity::new("c", "N")).unwrap();
6483        g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6484        g.add_relationship(Relationship::new("a", "c", "enemy", 0.5)).unwrap();
6485        let edges = g.edges_from(&EntityId::new("a")).unwrap();
6486        assert_eq!(edges.len(), 2);
6487    }
6488
6489    #[test]
6490    fn test_edges_from_returns_empty_for_unknown_entity() {
6491        let g = GraphStore::new();
6492        assert!(g.edges_from(&EntityId::new("missing")).unwrap().is_empty());
6493    }
6494
6495    #[test]
6496    fn test_neighbors_of_returns_sorted_unique_targets() {
6497        let g = GraphStore::new();
6498        g.add_entity(Entity::new("a", "N")).unwrap();
6499        g.add_entity(Entity::new("b", "N")).unwrap();
6500        g.add_entity(Entity::new("c", "N")).unwrap();
6501        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6502        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6503        let neighbors = g.neighbors_of(&EntityId::new("a")).unwrap();
6504        assert_eq!(neighbors, vec![EntityId::new("b"), EntityId::new("c")]);
6505    }
6506
6507    #[test]
6508    fn test_neighbors_of_returns_empty_for_no_outgoing_edges() {
6509        let g = GraphStore::new();
6510        g.add_entity(Entity::new("a", "N")).unwrap();
6511        assert!(g.neighbors_of(&EntityId::new("a")).unwrap().is_empty());
6512    }
6513
6514    // ── Round 40: EntityId From/FromStr/Deref, Entity remove_property ─────────
6515
6516    #[test]
6517    fn test_entity_id_from_string() {
6518        let id = EntityId::from("node-1".to_owned());
6519        assert_eq!(id.as_str(), "node-1");
6520    }
6521
6522    #[test]
6523    fn test_entity_id_from_str_ref() {
6524        let id = EntityId::from("node-2");
6525        assert_eq!(id.as_str(), "node-2");
6526    }
6527
6528    #[test]
6529    fn test_entity_id_from_str_parse_rejects_empty() {
6530        let result: Result<EntityId, _> = "".parse();
6531        assert!(result.is_err());
6532    }
6533
6534    #[test]
6535    fn test_entity_id_from_str_parse_accepts_nonempty() {
6536        let id: EntityId = "alice".parse().unwrap();
6537        assert_eq!(id.as_str(), "alice");
6538    }
6539
6540    #[test]
6541    fn test_entity_id_deref_to_str() {
6542        let id = EntityId::new("deref-node");
6543        let s: &str = &id;
6544        assert_eq!(s, "deref-node");
6545    }
6546
6547    #[test]
6548    fn test_entity_id_deref_enables_str_methods() {
6549        let id = EntityId::new("hello-world");
6550        assert!(id.contains('-'));
6551        assert_eq!(id.len(), 11);
6552    }
6553
6554    #[test]
6555    fn test_entity_remove_property_returns_value() {
6556        let mut e = Entity::new("e1", "Person")
6557            .with_property("age", serde_json::json!(30));
6558        let removed = e.remove_property("age");
6559        assert_eq!(removed, Some(serde_json::json!(30)));
6560        assert!(!e.has_property("age"));
6561    }
6562
6563    #[test]
6564    fn test_entity_remove_property_returns_none_when_absent() {
6565        let mut e = Entity::new("e2", "Person");
6566        assert!(e.remove_property("nonexistent").is_none());
6567    }
6568
6569    #[test]
6570    fn test_entity_property_count() {
6571        let e = Entity::new("e3", "X")
6572            .with_property("a", serde_json::json!(1))
6573            .with_property("b", serde_json::json!(2));
6574        assert_eq!(e.property_count(), 2);
6575    }
6576
6577    #[test]
6578    fn test_entity_properties_is_empty_true_when_none() {
6579        let e = Entity::new("e4", "X");
6580        assert!(e.properties_is_empty());
6581    }
6582
6583    #[test]
6584    fn test_entity_properties_is_empty_false_when_has_props() {
6585        let e = Entity::new("e5", "X").with_property("k", serde_json::json!("v"));
6586        assert!(!e.properties_is_empty());
6587    }
6588
6589    // ── Round 40 ──────────────────────────────────────────────────────────────
6590
6591    #[test]
6592    fn test_relationship_type_counts_returns_correct_map() {
6593        let g = GraphStore::new();
6594        add(&g, "a"); add(&g, "b"); add(&g, "c");
6595        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6596        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
6597        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
6598        let counts = g.relationship_type_counts().unwrap();
6599        assert_eq!(counts.get("KNOWS"), Some(&2));
6600        assert_eq!(counts.get("LIKES"), Some(&1));
6601    }
6602
6603    #[test]
6604    fn test_relationship_type_counts_empty_graph_returns_empty_map() {
6605        let g = GraphStore::new();
6606        assert!(g.relationship_type_counts().unwrap().is_empty());
6607    }
6608
6609    #[test]
6610    fn test_entities_without_property_returns_nodes_missing_key() {
6611        let g = GraphStore::new();
6612        g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
6613        g.add_entity(Entity::new("b", "N")).unwrap();
6614        g.add_entity(Entity::new("c", "N")).unwrap();
6615        let result = g.entities_without_property("age").unwrap();
6616        assert_eq!(result.len(), 2);
6617        assert!(result.iter().all(|e| !e.properties.contains_key("age")));
6618    }
6619
6620    #[test]
6621    fn test_entities_without_property_empty_when_all_have_key() {
6622        let g = GraphStore::new();
6623        g.add_entity(Entity::new("a", "N").with_property("role", serde_json::json!("admin"))).unwrap();
6624        assert!(g.entities_without_property("role").unwrap().is_empty());
6625    }
6626
6627    // ── Round 40 (continued): min_out_degree, min_in_degree, relationship_kinds_from ──
6628
6629    #[test]
6630    fn test_min_out_degree_returns_zero_when_sink_present() {
6631        let g = GraphStore::new();
6632        add(&g, "a"); add(&g, "b"); add(&g, "c");
6633        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6634        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6635        // b and c have out-degree 0; a has out-degree 2
6636        assert_eq!(g.min_out_degree().unwrap(), 0);
6637    }
6638
6639    #[test]
6640    fn test_min_out_degree_returns_zero_for_empty_graph() {
6641        let g = GraphStore::new();
6642        assert_eq!(g.min_out_degree().unwrap(), 0);
6643    }
6644
6645    #[test]
6646    fn test_min_in_degree_returns_zero_when_source_present() {
6647        let g = GraphStore::new();
6648        add(&g, "a"); add(&g, "b");
6649        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6650        // a has in-degree 0; b has in-degree 1
6651        assert_eq!(g.min_in_degree().unwrap(), 0);
6652    }
6653
6654    #[test]
6655    fn test_min_in_degree_returns_zero_for_empty_graph() {
6656        let g = GraphStore::new();
6657        assert_eq!(g.min_in_degree().unwrap(), 0);
6658    }
6659
6660    #[test]
6661    fn test_relationship_kinds_from_returns_sorted_distinct_kinds() {
6662        let g = GraphStore::new();
6663        add(&g, "a"); add(&g, "b"); add(&g, "c");
6664        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6665        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
6666        g.add_relationship(Relationship::new("a", "b", "TRUSTS", 1.0)).unwrap();
6667        let kinds = g.relationship_kinds_from(&EntityId::new("a")).unwrap();
6668        assert_eq!(kinds, vec!["KNOWS", "LIKES", "TRUSTS"]);
6669    }
6670
6671    #[test]
6672    fn test_relationship_kinds_from_returns_empty_for_unknown_entity() {
6673        let g = GraphStore::new();
6674        assert!(g.relationship_kinds_from(&EntityId::new("missing")).unwrap().is_empty());
6675    }
6676
6677    #[test]
6678    fn test_relationship_kinds_from_deduplicates_kinds() {
6679        let g = GraphStore::new();
6680        add(&g, "x"); add(&g, "y"); add(&g, "z");
6681        g.add_relationship(Relationship::new("x", "y", "SAME", 1.0)).unwrap();
6682        g.add_relationship(Relationship::new("x", "z", "SAME", 0.5)).unwrap();
6683        let kinds = g.relationship_kinds_from(&EntityId::new("x")).unwrap();
6684        assert_eq!(kinds, vec!["SAME"]);
6685    }
6686
6687    // ── Round 41 ──────────────────────────────────────────────────────────────
6688
6689    #[test]
6690    fn test_unique_relationship_types_returns_sorted_deduped_types() {
6691        let g = GraphStore::new();
6692        add(&g, "a"); add(&g, "b"); add(&g, "c");
6693        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6694        g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
6695        g.add_relationship(Relationship::new("a", "c", "KNOWS", 0.5)).unwrap();
6696        let types = g.unique_relationship_types().unwrap();
6697        assert_eq!(types, vec!["KNOWS", "LIKES"]);
6698    }
6699
6700    #[test]
6701    fn test_unique_relationship_types_empty_graph_returns_empty() {
6702        let g = GraphStore::new();
6703        assert!(g.unique_relationship_types().unwrap().is_empty());
6704    }
6705
6706    #[test]
6707    fn test_avg_property_count_returns_correct_average() {
6708        let g = GraphStore::new();
6709        g.add_entity(Entity::new("a", "N")
6710            .with_property("x", serde_json::json!(1))
6711            .with_property("y", serde_json::json!(2))).unwrap();
6712        g.add_entity(Entity::new("b", "N")).unwrap(); // 0 properties
6713        // average = (2 + 0) / 2 = 1.0
6714        assert!((g.avg_property_count().unwrap() - 1.0).abs() < 1e-9);
6715    }
6716
6717    #[test]
6718    fn test_avg_property_count_empty_graph_returns_zero() {
6719        let g = GraphStore::new();
6720        assert_eq!(g.avg_property_count().unwrap(), 0.0);
6721    }
6722
6723    // ── Round 42 ──────────────────────────────────────────────────────────────
6724
6725    #[test]
6726    fn test_property_key_frequency_counts_correctly() {
6727        let g = GraphStore::new();
6728        g.add_entity(Entity::new("a", "N")
6729            .with_property("age", serde_json::json!(30))
6730            .with_property("role", serde_json::json!("admin"))).unwrap();
6731        g.add_entity(Entity::new("b", "N")
6732            .with_property("age", serde_json::json!(25))).unwrap();
6733        let freq = g.property_key_frequency().unwrap();
6734        assert_eq!(freq.get("age"), Some(&2));
6735        assert_eq!(freq.get("role"), Some(&1));
6736    }
6737
6738    #[test]
6739    fn test_property_key_frequency_empty_graph_returns_empty() {
6740        let g = GraphStore::new();
6741        assert!(g.property_key_frequency().unwrap().is_empty());
6742    }
6743
6744    // ── Round 41: GraphStore::has_edge ─────────────────────────────────────────
6745
6746    #[test]
6747    fn test_has_edge_returns_true_when_edge_exists() {
6748        let g = GraphStore::new();
6749        add(&g, "a"); add(&g, "b");
6750        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6751        assert!(g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
6752    }
6753
6754    #[test]
6755    fn test_has_edge_returns_false_when_no_edge() {
6756        let g = GraphStore::new();
6757        add(&g, "a"); add(&g, "b");
6758        assert!(!g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
6759    }
6760
6761    #[test]
6762    fn test_has_edge_is_directional() {
6763        let g = GraphStore::new();
6764        add(&g, "a"); add(&g, "b");
6765        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6766        // a→b exists but b→a does not
6767        assert!(!g.has_edge(&EntityId::new("b"), &EntityId::new("a")).unwrap());
6768    }
6769
6770    // ── Round 42: Display, total_degree, entity_property_keys ────────────────
6771
6772    #[test]
6773    fn test_entity_display_with_props() {
6774        let e = Entity::new("alice", "Person")
6775            .with_property("age", serde_json::json!(30));
6776        let s = e.to_string();
6777        assert!(s.contains("alice") && s.contains("Person") && s.contains("props=1"));
6778    }
6779
6780    #[test]
6781    fn test_entity_display_no_props() {
6782        let e = Entity::new("bob", "Node");
6783        assert_eq!(e.to_string(), "Entity[id='bob', label='Node', props=0]");
6784    }
6785
6786    #[test]
6787    fn test_relationship_display_format() {
6788        let r = Relationship::new("alice", "bob", "KNOWS", 1.5);
6789        let s = r.to_string();
6790        assert!(s.contains("alice") && s.contains("KNOWS") && s.contains("bob") && s.contains("1.50"));
6791    }
6792
6793    #[test]
6794    fn test_graph_total_degree_sum_of_in_and_out() {
6795        let g = GraphStore::new();
6796        g.add_entity(Entity::new("a", "N")).unwrap();
6797        g.add_entity(Entity::new("b", "N")).unwrap();
6798        g.add_entity(Entity::new("c", "N")).unwrap();
6799        g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6800        g.add_relationship(Relationship::new("c", "a", "e", 1.0)).unwrap();
6801        assert_eq!(g.total_degree(&EntityId::new("a")).unwrap(), 2);
6802    }
6803
6804    #[test]
6805    fn test_graph_total_degree_zero_for_isolated_node() {
6806        let g = GraphStore::new();
6807        g.add_entity(Entity::new("iso", "N")).unwrap();
6808        assert_eq!(g.total_degree(&EntityId::new("iso")).unwrap(), 0);
6809    }
6810
6811    #[test]
6812    fn test_graph_entity_property_keys_returns_sorted() {
6813        let g = GraphStore::new();
6814        let e = Entity::new("e1", "X")
6815            .with_property("z", serde_json::json!(1))
6816            .with_property("a", serde_json::json!(2))
6817            .with_property("m", serde_json::json!(3));
6818        g.add_entity(e).unwrap();
6819        assert_eq!(
6820            g.entity_property_keys(&EntityId::new("e1")).unwrap(),
6821            vec!["a", "m", "z"]
6822        );
6823    }
6824
6825    #[test]
6826    fn test_graph_entity_property_keys_empty_for_unknown() {
6827        let g = GraphStore::new();
6828        assert!(g.entity_property_keys(&EntityId::new("missing")).unwrap().is_empty());
6829    }
6830
6831    #[test]
6832    fn test_entity_property_keys_method_sorted() {
6833        let e = Entity::new("p", "T")
6834            .with_property("b", serde_json::json!(0))
6835            .with_property("a", serde_json::json!(0));
6836        let keys = e.property_keys();
6837        assert_eq!(keys, vec!["a", "b"]);
6838    }
6839
6840    // ── Round 43 ──────────────────────────────────────────────────────────────
6841
6842    #[test]
6843    fn test_entities_sorted_by_label_returns_alphabetical_order() {
6844        let g = GraphStore::new();
6845        g.add_entity(Entity::new("1", "Zebra")).unwrap();
6846        g.add_entity(Entity::new("2", "Apple")).unwrap();
6847        g.add_entity(Entity::new("3", "Mango")).unwrap();
6848        let sorted = g.entities_sorted_by_label().unwrap();
6849        assert_eq!(sorted[0].label, "Apple");
6850        assert_eq!(sorted[1].label, "Mango");
6851        assert_eq!(sorted[2].label, "Zebra");
6852    }
6853
6854    #[test]
6855    fn test_entities_sorted_by_label_empty_graph_returns_empty() {
6856        let g = GraphStore::new();
6857        assert!(g.entities_sorted_by_label().unwrap().is_empty());
6858    }
6859
6860    // ── Round 42: entities_with_property, total_relationship_count ─────────────
6861
6862    #[test]
6863    fn test_entities_with_property_returns_entities_with_key() {
6864        let g = GraphStore::new();
6865        g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
6866        g.add_entity(Entity::new("b", "N").with_property("name", serde_json::json!("bob"))).unwrap();
6867        g.add_entity(Entity::new("c", "N").with_property("age", serde_json::json!(25))).unwrap();
6868        let result = g.entities_with_property("age").unwrap();
6869        assert_eq!(result.len(), 2);
6870        assert!(result.iter().all(|e| e.properties.contains_key("age")));
6871    }
6872
6873    #[test]
6874    fn test_entities_with_property_returns_empty_when_no_match() {
6875        let g = GraphStore::new();
6876        add(&g, "a");
6877        assert!(g.entities_with_property("missing_key").unwrap().is_empty());
6878    }
6879
6880    #[test]
6881    fn test_total_relationship_count_returns_edge_count() {
6882        let g = GraphStore::new();
6883        add(&g, "a"); add(&g, "b"); add(&g, "c");
6884        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6885        g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
6886        assert_eq!(g.total_relationship_count().unwrap(), 2);
6887    }
6888
6889    #[test]
6890    fn test_total_relationship_count_zero_for_empty_graph() {
6891        let g = GraphStore::new();
6892        assert_eq!(g.total_relationship_count().unwrap(), 0);
6893    }
6894
6895    // ── Round 43: entities_without_outgoing, entities_without_incoming ─────────
6896
6897    #[test]
6898    fn test_entities_without_outgoing_returns_nodes_with_no_out_edges() {
6899        let g = GraphStore::new();
6900        add(&g, "a"); add(&g, "b");
6901        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6902        // a has outgoing, b does not
6903        let no_out = g.entities_without_outgoing().unwrap();
6904        assert_eq!(no_out.len(), 1);
6905        assert_eq!(no_out[0].id, EntityId::new("b"));
6906    }
6907
6908    #[test]
6909    fn test_entities_without_outgoing_all_returned_for_empty_graph() {
6910        let g = GraphStore::new();
6911        add(&g, "x");
6912        let result = g.entities_without_outgoing().unwrap();
6913        assert_eq!(result.len(), 1);
6914    }
6915
6916    #[test]
6917    fn test_entities_without_incoming_returns_nodes_with_no_in_edges() {
6918        let g = GraphStore::new();
6919        add(&g, "a"); add(&g, "b");
6920        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6921        // a has no incoming; b has incoming from a
6922        let no_in = g.entities_without_incoming().unwrap();
6923        assert_eq!(no_in.len(), 1);
6924        assert_eq!(no_in[0].id, EntityId::new("a"));
6925    }
6926
6927    #[test]
6928    fn test_entities_without_incoming_all_returned_for_isolated_node() {
6929        let g = GraphStore::new();
6930        add(&g, "x");
6931        assert_eq!(g.entities_without_incoming().unwrap().len(), 1);
6932    }
6933
6934    // ── Round 44: path_to_string, Relationship::with_weight ──────────────────
6935
6936    #[test]
6937    fn test_path_to_string_uses_labels_when_entities_exist() {
6938        let g = GraphStore::new();
6939        let mut e1 = Entity::new("a", "Alice");
6940        let mut e2 = Entity::new("b", "Bob");
6941        let mut e3 = Entity::new("c", "Carol");
6942        e1.label = "Alice".into();
6943        e2.label = "Bob".into();
6944        e3.label = "Carol".into();
6945        g.add_entity(e1).unwrap();
6946        g.add_entity(e2).unwrap();
6947        g.add_entity(e3).unwrap();
6948        let path = vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")];
6949        let s = g.path_to_string(&path).unwrap();
6950        assert_eq!(s, "Alice \u{2192} Bob \u{2192} Carol");
6951    }
6952
6953    #[test]
6954    fn test_path_to_string_falls_back_to_id_for_unknown_entities() {
6955        let g = GraphStore::new();
6956        let path = vec![EntityId::new("x"), EntityId::new("y")];
6957        let s = g.path_to_string(&path).unwrap();
6958        assert!(s.contains("x") && s.contains("y"));
6959    }
6960
6961    #[test]
6962    fn test_path_to_string_empty_path_returns_empty_string() {
6963        let g = GraphStore::new();
6964        assert_eq!(g.path_to_string(&[]).unwrap(), "");
6965    }
6966
6967    #[test]
6968    fn test_relationship_with_weight_changes_weight() {
6969        let rel = Relationship::new("a", "b", "KNOWS", 1.0).with_weight(0.25);
6970        assert_eq!(rel.weight, 0.25);
6971        assert_eq!(rel.from, EntityId::new("a"));
6972        assert_eq!(rel.kind, "KNOWS");
6973    }
6974
6975    #[test]
6976    fn test_relationship_with_weight_zero_allowed() {
6977        let rel = Relationship::new("x", "y", "EDGE", 5.0).with_weight(0.0);
6978        assert_eq!(rel.weight, 0.0);
6979    }
6980
6981    // ── Round 44: total_out_degree, relationships_with_weight_above ────────────
6982
6983    #[test]
6984    fn test_total_out_degree_sums_all_outgoing_edges() {
6985        let g = GraphStore::new();
6986        add(&g, "a");
6987        add(&g, "b");
6988        add(&g, "c");
6989        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6990        g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
6991        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
6992        assert_eq!(g.total_out_degree().unwrap(), 3);
6993    }
6994
6995    #[test]
6996    fn test_total_out_degree_zero_for_empty_graph() {
6997        let g = GraphStore::new();
6998        assert_eq!(g.total_out_degree().unwrap(), 0);
6999    }
7000
7001    #[test]
7002    fn test_relationships_with_weight_above_filters_correctly() {
7003        let g = GraphStore::new();
7004        add(&g, "a");
7005        add(&g, "b");
7006        add(&g, "c");
7007        g.add_relationship(Relationship::new("a", "b", "EDGE", 0.5)).unwrap();
7008        g.add_relationship(Relationship::new("a", "c", "EDGE", 1.5)).unwrap();
7009        let heavy = g.relationships_with_weight_above(1.0).unwrap();
7010        assert_eq!(heavy.len(), 1);
7011        assert_eq!(heavy[0].to, EntityId::new("c"));
7012    }
7013
7014    #[test]
7015    fn test_relationships_with_weight_above_returns_empty_when_none_qualify() {
7016        let g = GraphStore::new();
7017        add(&g, "a");
7018        add(&g, "b");
7019        g.add_relationship(Relationship::new("a", "b", "EDGE", 0.3)).unwrap();
7020        assert!(g.relationships_with_weight_above(1.0).unwrap().is_empty());
7021    }
7022
7023    // ── Round 45: relationships_of_kind, entities_without_label ───────────────
7024
7025    #[test]
7026    fn test_relationships_of_kind_returns_matching_edges() {
7027        let g = GraphStore::new();
7028        add(&g, "a"); add(&g, "b"); add(&g, "c");
7029        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7030        g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
7031        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7032        let knows = g.relationships_of_kind("KNOWS").unwrap();
7033        assert_eq!(knows.len(), 2);
7034        assert!(knows.iter().all(|r| r.kind == "KNOWS"));
7035    }
7036
7037    #[test]
7038    fn test_relationships_of_kind_returns_empty_for_unknown_kind() {
7039        let g = GraphStore::new();
7040        add(&g, "a"); add(&g, "b");
7041        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7042        assert!(g.relationships_of_kind("MISSING").unwrap().is_empty());
7043    }
7044
7045    #[test]
7046    fn test_entities_without_label_excludes_matching_entities() {
7047        let g = GraphStore::new();
7048        let mut e1 = Entity::new("a", "Person");
7049        let mut e2 = Entity::new("b", "Company");
7050        let mut e3 = Entity::new("c", "Person");
7051        e1.label = "Person".into();
7052        e2.label = "Company".into();
7053        e3.label = "Person".into();
7054        g.add_entity(e1).unwrap();
7055        g.add_entity(e2).unwrap();
7056        g.add_entity(e3).unwrap();
7057        let non_persons = g.entities_without_label("Person").unwrap();
7058        assert_eq!(non_persons.len(), 1);
7059        assert_eq!(non_persons[0].id, EntityId::new("b"));
7060    }
7061
7062    #[test]
7063    fn test_entities_without_label_returns_all_when_no_match() {
7064        let g = GraphStore::new();
7065        add(&g, "x");
7066        assert_eq!(g.entities_without_label("NonExistent").unwrap().len(), 1);
7067    }
7068
7069    // ── Round 45: relationships_of_kind, entity_count_with_label ──────────────
7070
7071    #[test]
7072    fn test_relationships_of_kind_returns_matching_relationships() {
7073        let g = GraphStore::new();
7074        add(&g, "a");
7075        add(&g, "b");
7076        add(&g, "c");
7077        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7078        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7079        let knows = g.relationships_of_kind("KNOWS").unwrap();
7080        assert_eq!(knows.len(), 1);
7081        assert_eq!(knows[0].to, EntityId::new("b"));
7082    }
7083
7084    #[test]
7085    fn test_relationships_of_kind_empty_when_none_match() {
7086        let g = GraphStore::new();
7087        add(&g, "a");
7088        add(&g, "b");
7089        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7090        assert!(g.relationships_of_kind("HATES").unwrap().is_empty());
7091    }
7092
7093    #[test]
7094    fn test_entity_count_with_label_counts_matching_entities() {
7095        let g = GraphStore::new();
7096        g.add_entity(Entity::new("a", "Person")).unwrap();
7097        g.add_entity(Entity::new("b", "Person")).unwrap();
7098        g.add_entity(Entity::new("c", "Organization")).unwrap();
7099        assert_eq!(g.entity_count_with_label("Person").unwrap(), 2);
7100        assert_eq!(g.entity_count_with_label("Organization").unwrap(), 1);
7101    }
7102
7103    #[test]
7104    fn test_entity_count_with_label_zero_for_no_match() {
7105        let g = GraphStore::new();
7106        add(&g, "a");
7107        assert_eq!(g.entity_count_with_label("Alien").unwrap(), 0);
7108    }
7109
7110    // ── Round 44: edge_count_above_weight, entities_with_label_prefix, bidirectional_pairs
7111
7112    #[test]
7113    fn test_edge_count_above_weight_counts_heavy_edges() {
7114        let g = GraphStore::new();
7115        add(&g, "a"); add(&g, "b"); add(&g, "c");
7116        g.add_relationship(Relationship::new("a", "b", "K", 0.9)).unwrap();
7117        g.add_relationship(Relationship::new("b", "c", "K", 0.3)).unwrap();
7118        g.add_relationship(Relationship::new("a", "c", "K", 1.5)).unwrap();
7119        assert_eq!(g.edge_count_above_weight(0.8).unwrap(), 2); // 0.9 and 1.5
7120    }
7121
7122    #[test]
7123    fn test_edge_count_above_weight_zero_for_empty_graph() {
7124        let g = GraphStore::new();
7125        assert_eq!(g.edge_count_above_weight(0.0).unwrap(), 0);
7126    }
7127
7128    #[test]
7129    fn test_entities_with_label_prefix_returns_matching_entities() {
7130        let g = GraphStore::new();
7131        g.add_entity(Entity::new("a", "Person")).unwrap();
7132        g.add_entity(Entity::new("b", "Pet")).unwrap();
7133        g.add_entity(Entity::new("c", "Organization")).unwrap();
7134        let result = g.entities_with_label_prefix("Pe").unwrap();
7135        assert_eq!(result.len(), 2);
7136        assert!(result.iter().all(|e| e.label.starts_with("Pe")));
7137    }
7138
7139    #[test]
7140    fn test_entities_with_label_prefix_empty_when_no_match() {
7141        let g = GraphStore::new();
7142        add(&g, "a");
7143        assert!(g.entities_with_label_prefix("XYZ").unwrap().is_empty());
7144    }
7145
7146    #[test]
7147    fn test_bidirectional_pairs_returns_pairs_with_edges_in_both_directions() {
7148        let g = GraphStore::new();
7149        add(&g, "a"); add(&g, "b"); add(&g, "c");
7150        g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
7151        g.add_relationship(Relationship::new("b", "a", "K", 1.0)).unwrap(); // bidirectional
7152        g.add_relationship(Relationship::new("a", "c", "K", 1.0)).unwrap(); // one-way
7153        let pairs = g.bidirectional_pairs().unwrap();
7154        assert_eq!(pairs.len(), 1);
7155    }
7156
7157    #[test]
7158    fn test_bidirectional_pairs_empty_for_one_directional_graph() {
7159        let g = GraphStore::new();
7160        add(&g, "a"); add(&g, "b");
7161        g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
7162        assert!(g.bidirectional_pairs().unwrap().is_empty());
7163    }
7164
7165    // ── Round 45: entities_with_min_out_degree ─────────────────────────────────
7166
7167    #[test]
7168    fn test_entities_with_min_out_degree_filters_correctly() {
7169        let g = GraphStore::new();
7170        add(&g, "a"); add(&g, "b"); add(&g, "c");
7171        g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
7172        g.add_relationship(Relationship::new("a", "c", "R", 1.0)).unwrap();
7173        // "a" has out-degree 2, "b" and "c" have out-degree 0
7174        let result = g.entities_with_min_out_degree(2).unwrap();
7175        assert_eq!(result.len(), 1);
7176        assert_eq!(result[0].id.as_str(), "a");
7177    }
7178
7179    #[test]
7180    fn test_entities_with_min_out_degree_zero_includes_all() {
7181        let g = GraphStore::new();
7182        add(&g, "x"); add(&g, "y");
7183        assert_eq!(g.entities_with_min_out_degree(0).unwrap().len(), 2);
7184    }
7185
7186    #[test]
7187    fn test_entities_with_min_out_degree_empty_for_empty_graph() {
7188        let g = GraphStore::new();
7189        assert!(g.entities_with_min_out_degree(1).unwrap().is_empty());
7190    }
7191
7192    // ── Round 46: mean_in_degree, entity_count_by_label_prefix ────────────────
7193
7194    #[test]
7195    fn test_mean_in_degree_computes_average() {
7196        let g = GraphStore::new();
7197        add(&g, "a"); add(&g, "b"); add(&g, "c");
7198        g.add_relationship(Relationship::new("a", "b", "EDGE", 1.0)).unwrap();
7199        g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
7200        // 2 total in-degree across 3 entities → mean = 2/3
7201        let mean = g.mean_in_degree().unwrap();
7202        assert!((mean - 2.0 / 3.0).abs() < 1e-9);
7203    }
7204
7205    #[test]
7206    fn test_mean_in_degree_zero_for_empty_graph() {
7207        let g = GraphStore::new();
7208        assert_eq!(g.mean_in_degree().unwrap(), 0.0);
7209    }
7210
7211    #[test]
7212    fn test_entity_count_by_label_prefix_counts_matching_entities() {
7213        let g = GraphStore::new();
7214        g.add_entity(Entity::new("a", "Person-Alice")).unwrap();
7215        g.add_entity(Entity::new("b", "Person-Bob")).unwrap();
7216        g.add_entity(Entity::new("c", "Organization")).unwrap();
7217        assert_eq!(g.entity_count_by_label_prefix("Person").unwrap(), 2);
7218        assert_eq!(g.entity_count_by_label_prefix("Org").unwrap(), 1);
7219    }
7220
7221    #[test]
7222    fn test_entity_count_by_label_prefix_zero_for_no_match() {
7223        let g = GraphStore::new();
7224        add(&g, "x");
7225        assert_eq!(g.entity_count_by_label_prefix("Alien").unwrap(), 0);
7226    }
7227
7228    // ── Round 47: relationship_weight_sum, label_frequency ────────────────────
7229
7230    #[test]
7231    fn test_relationship_weight_sum_sums_all_weights() {
7232        let g = GraphStore::new();
7233        add(&g, "a"); add(&g, "b"); add(&g, "c");
7234        g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
7235        g.add_relationship(Relationship::new("a", "c", "E", 3.0)).unwrap();
7236        assert!((g.relationship_weight_sum().unwrap() - 5.0).abs() < 1e-6);
7237    }
7238
7239    #[test]
7240    fn test_relationship_weight_sum_zero_for_empty_graph() {
7241        let g = GraphStore::new();
7242        assert_eq!(g.relationship_weight_sum().unwrap(), 0.0);
7243    }
7244
7245    #[test]
7246    fn test_label_frequency_counts_labels() {
7247        let g = GraphStore::new();
7248        g.add_entity(Entity::new("a", "Person")).unwrap();
7249        g.add_entity(Entity::new("b", "Person")).unwrap();
7250        g.add_entity(Entity::new("c", "Node")).unwrap();
7251        let freq = g.label_frequency().unwrap();
7252        assert_eq!(*freq.get("Person").unwrap(), 2);
7253        assert_eq!(*freq.get("Node").unwrap(), 1);
7254    }
7255
7256    // ── Round 48: entities_sorted_by_id ────────────────────────────────────────
7257
7258    #[test]
7259    fn test_entities_sorted_by_id_returns_alphabetical_order() {
7260        let g = GraphStore::new();
7261        g.add_entity(Entity::new("charlie", "Node")).unwrap();
7262        g.add_entity(Entity::new("alice", "Node")).unwrap();
7263        g.add_entity(Entity::new("bob", "Node")).unwrap();
7264        let sorted = g.entities_sorted_by_id().unwrap();
7265        let ids: Vec<&str> = sorted.iter().map(|e| e.id.as_str()).collect();
7266        assert_eq!(ids, vec!["alice", "bob", "charlie"]);
7267    }
7268
7269    #[test]
7270    fn test_entities_sorted_by_id_empty_for_empty_graph() {
7271        let g = GraphStore::new();
7272        assert!(g.entities_sorted_by_id().unwrap().is_empty());
7273    }
7274
7275    #[test]
7276    fn test_label_frequency_empty_for_empty_graph() {
7277        let g = GraphStore::new();
7278        assert!(g.label_frequency().unwrap().is_empty());
7279    }
7280
7281    // ── Round 49: entities_with_label_containing ───────────────────────────────
7282
7283    #[test]
7284    fn test_entities_with_label_containing_returns_matching_entities() {
7285        let g = GraphStore::new();
7286        g.add_entity(Entity::new("a", "PersonA")).unwrap();
7287        g.add_entity(Entity::new("b", "PersonB")).unwrap();
7288        g.add_entity(Entity::new("c", "Location")).unwrap();
7289        let mut result = g.entities_with_label_containing("Person").unwrap();
7290        result.sort_unstable_by(|a, b| a.id.cmp(&b.id));
7291        assert_eq!(result.len(), 2);
7292        assert_eq!(result[0].id.as_str(), "a");
7293        assert_eq!(result[1].id.as_str(), "b");
7294    }
7295
7296    #[test]
7297    fn test_entities_with_label_containing_empty_when_no_match() {
7298        let g = GraphStore::new();
7299        g.add_entity(Entity::new("a", "Node")).unwrap();
7300        assert!(g.entities_with_label_containing("Person").unwrap().is_empty());
7301    }
7302
7303    // ── Round 47: entities_with_min_in_degree ─────────────────────────────────
7304
7305    #[test]
7306    fn test_entities_with_min_in_degree_returns_correct_entities() {
7307        let g = GraphStore::new();
7308        g.add_entity(Entity::new("a", "Node")).unwrap();
7309        g.add_entity(Entity::new("b", "Node")).unwrap();
7310        g.add_entity(Entity::new("c", "Node")).unwrap();
7311        // Two edges target "b": a→b and c→b
7312        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
7313        g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
7314        // One edge targets "c": a→c
7315        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
7316        // min_in_degree=2 → only "b"
7317        let result = g.entities_with_min_in_degree(2).unwrap();
7318        assert_eq!(result.len(), 1);
7319        assert_eq!(result[0].id.as_str(), "b");
7320    }
7321
7322    #[test]
7323    fn test_entities_with_min_in_degree_zero_includes_all() {
7324        let g = GraphStore::new();
7325        g.add_entity(Entity::new("a", "Node")).unwrap();
7326        g.add_entity(Entity::new("b", "Node")).unwrap();
7327        assert_eq!(g.entities_with_min_in_degree(0).unwrap().len(), 2);
7328    }
7329
7330    #[test]
7331    fn test_entities_with_min_in_degree_empty_for_empty_graph() {
7332        let g = GraphStore::new();
7333        assert!(g.entities_with_min_in_degree(1).unwrap().is_empty());
7334    }
7335
7336    // ── Round 49: total_property_count, entities_with_no_properties ───────────
7337
7338    #[test]
7339    fn test_total_property_count_sums_across_entities() {
7340        let g = GraphStore::new();
7341        g.add_entity(
7342            Entity::new("a", "Node")
7343                .with_property("x", serde_json::json!(1))
7344                .with_property("y", serde_json::json!(2)),
7345        )
7346        .unwrap();
7347        g.add_entity(Entity::new("b", "Node").with_property("z", serde_json::json!(3))).unwrap();
7348        assert_eq!(g.total_property_count().unwrap(), 3);
7349    }
7350
7351    #[test]
7352    fn test_total_property_count_zero_for_empty_graph() {
7353        let g = GraphStore::new();
7354        assert_eq!(g.total_property_count().unwrap(), 0);
7355    }
7356
7357    #[test]
7358    fn test_entities_with_no_properties_returns_bare_entities() {
7359        let g = GraphStore::new();
7360        g.add_entity(Entity::new("bare", "Node")).unwrap();
7361        g.add_entity(
7362            Entity::new("annotated", "Node").with_property("k", serde_json::json!("v")),
7363        )
7364        .unwrap();
7365        let result = g.entities_with_no_properties().unwrap();
7366        assert_eq!(result.len(), 1);
7367        assert_eq!(result[0].id.as_str(), "bare");
7368    }
7369
7370    #[test]
7371    fn test_entities_with_no_properties_empty_when_all_have_properties() {
7372        let g = GraphStore::new();
7373        g.add_entity(Entity::new("a", "N").with_property("k", serde_json::json!(1))).unwrap();
7374        assert!(g.entities_with_no_properties().unwrap().is_empty());
7375    }
7376
7377    // ── Round 50: entity_neighbor_count ───────────────────────────────────────
7378
7379    #[test]
7380    fn test_entity_neighbor_count_returns_out_degree() {
7381        let g = GraphStore::new();
7382        g.add_entity(Entity::new("a", "N")).unwrap();
7383        g.add_entity(Entity::new("b", "N")).unwrap();
7384        g.add_entity(Entity::new("c", "N")).unwrap();
7385        g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7386        g.add_relationship(Relationship::new("a", "c", "E", 1.0)).unwrap();
7387        let a_id = EntityId("a".to_string());
7388        assert_eq!(g.entity_neighbor_count(&a_id).unwrap(), 2);
7389    }
7390
7391    #[test]
7392    fn test_entity_neighbor_count_zero_for_isolated_entity() {
7393        let g = GraphStore::new();
7394        g.add_entity(Entity::new("lone", "N")).unwrap();
7395        let id = EntityId("lone".to_string());
7396        assert_eq!(g.entity_neighbor_count(&id).unwrap(), 0);
7397    }
7398
7399    // ── Round 47: entity_by_label, distinct_relationship_kind_count ────────────
7400
7401    #[test]
7402    fn test_entity_by_label_returns_matching_entity() {
7403        let g = GraphStore::new();
7404        g.add_entity(Entity::new("a", "Person")).unwrap();
7405        let result = g.entity_by_label("Person").unwrap();
7406        assert!(result.is_some());
7407        assert_eq!(result.unwrap().id.as_str(), "a");
7408    }
7409
7410    #[test]
7411    fn test_entity_by_label_returns_none_when_not_found() {
7412        let g = GraphStore::new();
7413        g.add_entity(Entity::new("a", "Node")).unwrap();
7414        assert!(g.entity_by_label("Missing").unwrap().is_none());
7415    }
7416
7417    #[test]
7418    fn test_distinct_relationship_kind_count_counts_unique_kinds() {
7419        let g = GraphStore::new();
7420        g.add_entity(Entity::new("a", "N")).unwrap();
7421        g.add_entity(Entity::new("b", "N")).unwrap();
7422        g.add_entity(Entity::new("c", "N")).unwrap();
7423        g.add_relationship(Relationship::new("a", "b", "FRIEND", 1.0)).unwrap();
7424        g.add_relationship(Relationship::new("a", "c", "ENEMY", 1.0)).unwrap();
7425        assert_eq!(g.distinct_relationship_kind_count().unwrap(), 2);
7426    }
7427
7428    #[test]
7429    fn test_distinct_relationship_kind_count_zero_for_empty_graph() {
7430        let g = GraphStore::new();
7431        assert_eq!(g.distinct_relationship_kind_count().unwrap(), 0);
7432    }
7433
7434    // ── Round 50: weight_above_threshold_ratio, entities_sorted_by_out_degree ──
7435
7436    #[test]
7437    fn test_weight_above_threshold_ratio_returns_correct_fraction() {
7438        let g = GraphStore::new();
7439        g.add_entity(Entity::new("a", "N")).unwrap();
7440        g.add_entity(Entity::new("b", "N")).unwrap();
7441        g.add_entity(Entity::new("c", "N")).unwrap();
7442        g.add_relationship(Relationship::new("a", "b", "E", 0.8)).unwrap();
7443        g.add_relationship(Relationship::new("a", "c", "E", 0.3)).unwrap();
7444        // 1 out of 2 relationships has weight > 0.5
7445        let ratio = g.weight_above_threshold_ratio(0.5).unwrap();
7446        assert!((ratio - 0.5).abs() < 1e-9);
7447    }
7448
7449    #[test]
7450    fn test_weight_above_threshold_ratio_zero_for_empty_graph() {
7451        let g = GraphStore::new();
7452        assert_eq!(g.weight_above_threshold_ratio(0.5).unwrap(), 0.0);
7453    }
7454
7455    #[test]
7456    fn test_entities_sorted_by_out_degree_most_connected_first() {
7457        let g = GraphStore::new();
7458        g.add_entity(Entity::new("hub", "N")).unwrap();
7459        g.add_entity(Entity::new("leaf", "N")).unwrap();
7460        g.add_entity(Entity::new("mid", "N")).unwrap();
7461        g.add_relationship(Relationship::new("hub", "leaf", "E", 1.0)).unwrap();
7462        g.add_relationship(Relationship::new("hub", "mid", "E", 1.0)).unwrap();
7463        g.add_relationship(Relationship::new("mid", "leaf", "E", 1.0)).unwrap();
7464        let sorted = g.entities_sorted_by_out_degree().unwrap();
7465        assert_eq!(sorted[0].id.as_str(), "hub"); // out-degree 2
7466        assert_eq!(sorted[1].id.as_str(), "mid"); // out-degree 1
7467    }
7468
7469    #[test]
7470    fn test_entities_sorted_by_out_degree_empty_for_empty_graph() {
7471        let g = GraphStore::new();
7472        assert!(g.entities_sorted_by_out_degree().unwrap().is_empty());
7473    }
7474
7475    // ── Round 51: entity_labels_sorted, relationship_type_count ───────────────
7476
7477    #[test]
7478    fn test_entity_labels_sorted_returns_unique_labels_in_order() {
7479        let g = GraphStore::new();
7480        g.add_entity(Entity::new("a", "Zebra")).unwrap();
7481        g.add_entity(Entity::new("b", "Apple")).unwrap();
7482        g.add_entity(Entity::new("c", "Zebra")).unwrap(); // duplicate
7483        let labels = g.entity_labels_sorted().unwrap();
7484        assert_eq!(labels, vec!["Apple", "Zebra"]);
7485    }
7486
7487    #[test]
7488    fn test_entity_labels_sorted_empty_for_empty_graph() {
7489        let g = GraphStore::new();
7490        assert!(g.entity_labels_sorted().unwrap().is_empty());
7491    }
7492
7493    #[test]
7494    fn test_relationship_type_count_counts_distinct_kinds() {
7495        let g = GraphStore::new();
7496        g.add_entity(Entity::new("a", "N")).unwrap();
7497        g.add_entity(Entity::new("b", "N")).unwrap();
7498        g.add_entity(Entity::new("c", "N")).unwrap();
7499        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7500        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7501        assert_eq!(g.relationship_type_count().unwrap(), 2);
7502    }
7503
7504    #[test]
7505    fn test_relationship_type_count_zero_for_empty_graph() {
7506        let g = GraphStore::new();
7507        assert_eq!(g.relationship_type_count().unwrap(), 0);
7508    }
7509
7510    // ── Round 52 ──────────────────────────────────────────────────────────────
7511
7512    #[test]
7513    fn test_has_entity_with_label_true_when_present() {
7514        let g = GraphStore::new();
7515        g.add_entity(Entity::new("a", "Person")).unwrap();
7516        assert!(g.has_entity_with_label("Person").unwrap());
7517    }
7518
7519    #[test]
7520    fn test_has_entity_with_label_false_when_absent() {
7521        let g = GraphStore::new();
7522        g.add_entity(Entity::new("a", "Person")).unwrap();
7523        assert!(!g.has_entity_with_label("Robot").unwrap());
7524    }
7525
7526    #[test]
7527    fn test_has_entity_with_label_false_for_empty_graph() {
7528        let g = GraphStore::new();
7529        assert!(!g.has_entity_with_label("Anything").unwrap());
7530    }
7531
7532    #[test]
7533    fn test_min_weight_returns_smallest_weight() {
7534        let g = GraphStore::new();
7535        g.add_entity(Entity::new("a", "N")).unwrap();
7536        g.add_entity(Entity::new("b", "N")).unwrap();
7537        g.add_entity(Entity::new("c", "N")).unwrap();
7538        g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
7539        g.add_relationship(Relationship::new("b", "c", "k", 2.0)).unwrap();
7540        assert_eq!(g.min_weight().unwrap(), Some(0.5));
7541    }
7542
7543    #[test]
7544    fn test_min_weight_none_for_graph_with_no_relationships() {
7545        let g = GraphStore::new();
7546        g.add_entity(Entity::new("a", "N")).unwrap();
7547        assert_eq!(g.min_weight().unwrap(), None);
7548    }
7549
7550    // ── Round 52: entities_with_exact_label ───────────────────────────────────
7551
7552    #[test]
7553    fn test_entities_with_exact_label_returns_matching_entities() {
7554        let g = GraphStore::new();
7555        g.add_entity(Entity::new("a", "Person")).unwrap();
7556        g.add_entity(Entity::new("b", "Person")).unwrap();
7557        g.add_entity(Entity::new("c", "Robot")).unwrap();
7558        let people = g.entities_with_exact_label("Person").unwrap();
7559        assert_eq!(people.len(), 2);
7560        assert!(people.iter().all(|e| e.label == "Person"));
7561    }
7562
7563    #[test]
7564    fn test_entities_with_exact_label_empty_when_no_match() {
7565        let g = GraphStore::new();
7566        g.add_entity(Entity::new("a", "Person")).unwrap();
7567        assert!(g.entities_with_exact_label("Robot").unwrap().is_empty());
7568    }
7569
7570    #[test]
7571    fn test_entities_with_exact_label_empty_for_empty_graph() {
7572        let g = GraphStore::new();
7573        assert!(g.entities_with_exact_label("Person").unwrap().is_empty());
7574    }
7575
7576    // ── Round 53: average_weight, max_weight ──────────────────────────────────
7577
7578    #[test]
7579    fn test_average_weight_returns_mean_of_all_edges() {
7580        let g = GraphStore::new();
7581        g.add_entity(Entity::new("a", "N")).unwrap();
7582        g.add_entity(Entity::new("b", "N")).unwrap();
7583        g.add_entity(Entity::new("c", "N")).unwrap();
7584        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7585        g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
7586        assert_eq!(g.average_weight().unwrap(), 2.0);
7587    }
7588
7589    #[test]
7590    fn test_average_weight_zero_for_graph_with_no_edges() {
7591        let g = GraphStore::new();
7592        g.add_entity(Entity::new("a", "N")).unwrap();
7593        assert_eq!(g.average_weight().unwrap(), 0.0);
7594    }
7595
7596    #[test]
7597    fn test_max_weight_returns_largest_weight() {
7598        let g = GraphStore::new();
7599        g.add_entity(Entity::new("a", "N")).unwrap();
7600        g.add_entity(Entity::new("b", "N")).unwrap();
7601        g.add_entity(Entity::new("c", "N")).unwrap();
7602        g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
7603        g.add_relationship(Relationship::new("b", "c", "k", 10.0)).unwrap();
7604        assert_eq!(g.max_weight().unwrap(), Some(10.0));
7605    }
7606
7607    #[test]
7608    fn test_max_weight_none_for_empty_graph() {
7609        let g = GraphStore::new();
7610        assert_eq!(g.max_weight().unwrap(), None);
7611    }
7612
7613    // ── Round 53 ──────────────────────────────────────────────────────────────
7614
7615    #[test]
7616    fn test_relationships_of_kind_count_returns_correct_count() {
7617        let g = GraphStore::new();
7618        g.add_entity(Entity::new("a", "N")).unwrap();
7619        g.add_entity(Entity::new("b", "N")).unwrap();
7620        g.add_entity(Entity::new("c", "N")).unwrap();
7621        g.add_relationship(Relationship::new("a", "b", "FOLLOWS", 1.0)).unwrap();
7622        g.add_relationship(Relationship::new("b", "c", "FOLLOWS", 1.0)).unwrap();
7623        g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7624        assert_eq!(g.relationships_of_kind_count("FOLLOWS").unwrap(), 2);
7625        assert_eq!(g.relationships_of_kind_count("LIKES").unwrap(), 1);
7626    }
7627
7628    #[test]
7629    fn test_relationships_of_kind_count_zero_for_absent_kind() {
7630        let g = GraphStore::new();
7631        g.add_entity(Entity::new("a", "N")).unwrap();
7632        g.add_entity(Entity::new("b", "N")).unwrap();
7633        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7634        assert_eq!(g.relationships_of_kind_count("MISSING").unwrap(), 0);
7635    }
7636
7637    #[test]
7638    fn test_entities_with_incoming_returns_targets() {
7639        let g = GraphStore::new();
7640        g.add_entity(Entity::new("src", "N")).unwrap();
7641        g.add_entity(Entity::new("dst", "N")).unwrap();
7642        g.add_entity(Entity::new("iso", "N")).unwrap();
7643        g.add_relationship(Relationship::new("src", "dst", "E", 1.0)).unwrap();
7644        let with_in: Vec<_> = g.entities_with_incoming().unwrap();
7645        let ids: Vec<&str> = with_in.iter().map(|e| e.id.as_str()).collect();
7646        assert!(ids.contains(&"dst"));
7647        assert!(!ids.contains(&"src"));
7648        assert!(!ids.contains(&"iso"));
7649    }
7650
7651    #[test]
7652    fn test_entities_with_incoming_empty_for_no_relationships() {
7653        let g = GraphStore::new();
7654        g.add_entity(Entity::new("a", "N")).unwrap();
7655        assert!(g.entities_with_incoming().unwrap().is_empty());
7656    }
7657
7658    // ── Round 48 ──────────────────────────────────────────────────────────────
7659
7660    #[test]
7661    fn test_entities_with_no_relationships_returns_sink_nodes() {
7662        let g = GraphStore::new();
7663        g.add_entity(Entity::new("src", "N")).unwrap();
7664        g.add_entity(Entity::new("sink", "N")).unwrap();
7665        g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
7666        let sinks = g.entities_with_no_relationships().unwrap();
7667        let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
7668        assert!(ids.contains(&"sink"));
7669        assert!(!ids.contains(&"src"));
7670    }
7671
7672    #[test]
7673    fn test_entities_with_no_relationships_all_when_no_edges() {
7674        let g = GraphStore::new();
7675        g.add_entity(Entity::new("a", "N")).unwrap();
7676        g.add_entity(Entity::new("b", "N")).unwrap();
7677        assert_eq!(g.entities_with_no_relationships().unwrap().len(), 2);
7678    }
7679
7680    #[test]
7681    fn test_entities_with_no_relationships_empty_for_empty_graph() {
7682        let g = GraphStore::new();
7683        assert!(g.entities_with_no_relationships().unwrap().is_empty());
7684    }
7685
7686    // ── Round 54: entity_ids_sorted, relationship_count_for ───────────────────
7687
7688    #[test]
7689    fn test_entity_ids_sorted_returns_alphabetical_ids() {
7690        let g = GraphStore::new();
7691        g.add_entity(Entity::new("zebra", "N")).unwrap();
7692        g.add_entity(Entity::new("alpha", "N")).unwrap();
7693        g.add_entity(Entity::new("mango", "N")).unwrap();
7694        let ids = g.entity_ids_sorted().unwrap();
7695        assert_eq!(ids[0].0, "alpha");
7696        assert_eq!(ids[1].0, "mango");
7697        assert_eq!(ids[2].0, "zebra");
7698    }
7699
7700    #[test]
7701    fn test_entity_ids_sorted_empty_for_empty_graph() {
7702        let g = GraphStore::new();
7703        assert!(g.entity_ids_sorted().unwrap().is_empty());
7704    }
7705
7706    #[test]
7707    fn test_relationship_count_for_returns_out_degree() {
7708        let g = GraphStore::new();
7709        g.add_entity(Entity::new("a", "N")).unwrap();
7710        g.add_entity(Entity::new("b", "N")).unwrap();
7711        g.add_entity(Entity::new("c", "N")).unwrap();
7712        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7713        g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
7714        let a_id = EntityId("a".to_string());
7715        assert_eq!(g.relationship_count_for(&a_id).unwrap(), 2);
7716    }
7717
7718    #[test]
7719    fn test_relationship_count_for_zero_for_unknown_entity() {
7720        let g = GraphStore::new();
7721        let id = EntityId("unknown".to_string());
7722        assert_eq!(g.relationship_count_for(&id).unwrap(), 0);
7723    }
7724
7725    // ── Round 55: entity_pair_has_relationship, nodes_reachable_from ──────────
7726
7727    #[test]
7728    fn test_entity_pair_has_relationship_true_when_edge_exists() {
7729        let g = GraphStore::new();
7730        g.add_entity(Entity::new("a", "N")).unwrap();
7731        g.add_entity(Entity::new("b", "N")).unwrap();
7732        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7733        let a = EntityId("a".to_string());
7734        let b = EntityId("b".to_string());
7735        assert!(g.entity_pair_has_relationship(&a, &b).unwrap());
7736    }
7737
7738    #[test]
7739    fn test_entity_pair_has_relationship_false_when_no_edge() {
7740        let g = GraphStore::new();
7741        g.add_entity(Entity::new("a", "N")).unwrap();
7742        g.add_entity(Entity::new("b", "N")).unwrap();
7743        let a = EntityId("a".to_string());
7744        let b = EntityId("b".to_string());
7745        assert!(!g.entity_pair_has_relationship(&a, &b).unwrap());
7746    }
7747
7748    #[test]
7749    fn test_nodes_reachable_from_returns_all_reachable_nodes() {
7750        let g = GraphStore::new();
7751        g.add_entity(Entity::new("a", "N")).unwrap();
7752        g.add_entity(Entity::new("b", "N")).unwrap();
7753        g.add_entity(Entity::new("c", "N")).unwrap();
7754        g.add_entity(Entity::new("d", "N")).unwrap();
7755        g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7756        g.add_relationship(Relationship::new("b", "c", "E", 1.0)).unwrap();
7757        let start = EntityId("a".to_string());
7758        let reachable = g.nodes_reachable_from(&start).unwrap();
7759        let ids: Vec<&str> = reachable.iter().map(|id| id.0.as_str()).collect();
7760        assert!(ids.contains(&"b"));
7761        assert!(ids.contains(&"c"));
7762        assert!(!ids.contains(&"a"));
7763        assert!(!ids.contains(&"d"));
7764    }
7765
7766    #[test]
7767    fn test_nodes_reachable_from_empty_for_isolated_node() {
7768        let g = GraphStore::new();
7769        g.add_entity(Entity::new("iso", "N")).unwrap();
7770        let id = EntityId("iso".to_string());
7771        assert!(g.nodes_reachable_from(&id).unwrap().is_empty());
7772    }
7773
7774    // ── Round 54 ──────────────────────────────────────────────────────────────
7775
7776    #[test]
7777    fn test_self_loops_returns_loop_relationships() {
7778        let g = GraphStore::new();
7779        g.add_entity(Entity::new("a", "N")).unwrap();
7780        g.add_entity(Entity::new("b", "N")).unwrap();
7781        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
7782        g.add_relationship(Relationship::new("a", "b", "other", 1.0)).unwrap();
7783        let loops = g.self_loops().unwrap();
7784        assert_eq!(loops.len(), 1);
7785        assert_eq!(loops[0].from.as_str(), "a");
7786        assert_eq!(loops[0].to.as_str(), "a");
7787    }
7788
7789    #[test]
7790    fn test_self_loops_empty_when_no_loops() {
7791        let g = GraphStore::new();
7792        g.add_entity(Entity::new("a", "N")).unwrap();
7793        g.add_entity(Entity::new("b", "N")).unwrap();
7794        g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7795        assert!(g.self_loops().unwrap().is_empty());
7796    }
7797
7798    #[test]
7799    fn test_entities_with_label_and_property_returns_matching() {
7800        let g = GraphStore::new();
7801        let mut e = Entity::new("a", "Person");
7802        e.properties.insert("age".to_string(), serde_json::json!("30"));
7803        g.add_entity(e).unwrap();
7804        g.add_entity(Entity::new("b", "Person")).unwrap();
7805        g.add_entity(Entity::new("c", "Robot")).unwrap();
7806        let result = g.entities_with_label_and_property("Person", "age").unwrap();
7807        assert_eq!(result.len(), 1);
7808        assert_eq!(result[0].id.as_str(), "a");
7809    }
7810
7811    #[test]
7812    fn test_entities_with_label_and_property_empty_when_no_match() {
7813        let g = GraphStore::new();
7814        g.add_entity(Entity::new("a", "Person")).unwrap();
7815        assert!(g.entities_with_label_and_property("Person", "age").unwrap().is_empty());
7816    }
7817
7818    // ── Round 49 ──────────────────────────────────────────────────────────────
7819
7820    #[test]
7821    fn test_total_edge_weight_sums_all_weights() {
7822        let g = GraphStore::new();
7823        g.add_entity(Entity::new("a", "N")).unwrap();
7824        g.add_entity(Entity::new("b", "N")).unwrap();
7825        g.add_entity(Entity::new("c", "N")).unwrap();
7826        g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
7827        g.add_relationship(Relationship::new("b", "c", "E", 3.0)).unwrap();
7828        let total = g.total_edge_weight().unwrap();
7829        assert!((total - 5.0).abs() < 1e-9);
7830    }
7831
7832    #[test]
7833    fn test_total_edge_weight_zero_for_no_relationships() {
7834        let g = GraphStore::new();
7835        g.add_entity(Entity::new("a", "N")).unwrap();
7836        assert!((g.total_edge_weight().unwrap()).abs() < 1e-9);
7837    }
7838
7839    #[test]
7840    fn test_entity_with_max_out_degree_returns_highest_degree_entity() {
7841        let g = GraphStore::new();
7842        g.add_entity(Entity::new("hub", "N")).unwrap();
7843        g.add_entity(Entity::new("spoke1", "N")).unwrap();
7844        g.add_entity(Entity::new("spoke2", "N")).unwrap();
7845        g.add_entity(Entity::new("leaf", "N")).unwrap();
7846        g.add_relationship(Relationship::new("hub", "spoke1", "E", 1.0)).unwrap();
7847        g.add_relationship(Relationship::new("hub", "spoke2", "E", 1.0)).unwrap();
7848        g.add_relationship(Relationship::new("spoke1", "leaf", "E", 1.0)).unwrap();
7849        let top = g.entity_with_max_out_degree().unwrap().unwrap();
7850        assert_eq!(top.id.as_str(), "hub");
7851    }
7852
7853    #[test]
7854    fn test_entity_with_max_out_degree_none_for_empty_graph() {
7855        let g = GraphStore::new();
7856        assert!(g.entity_with_max_out_degree().unwrap().is_none());
7857    }
7858
7859    // ── Round 56: edge_weight_between, total_relationship_weight ──────────────
7860
7861    #[test]
7862    fn test_edge_weight_between_returns_weight_when_edge_exists() {
7863        let g = GraphStore::new();
7864        g.add_entity(Entity::new("a", "N")).unwrap();
7865        g.add_entity(Entity::new("b", "N")).unwrap();
7866        g.add_relationship(Relationship::new("a", "b", "k", 2.5)).unwrap();
7867        let a = EntityId("a".to_string());
7868        let b = EntityId("b".to_string());
7869        assert_eq!(g.edge_weight_between(&a, &b).unwrap(), Some(2.5));
7870    }
7871
7872    #[test]
7873    fn test_edge_weight_between_none_when_no_edge() {
7874        let g = GraphStore::new();
7875        g.add_entity(Entity::new("a", "N")).unwrap();
7876        g.add_entity(Entity::new("b", "N")).unwrap();
7877        let a = EntityId("a".to_string());
7878        let b = EntityId("b".to_string());
7879        assert_eq!(g.edge_weight_between(&a, &b).unwrap(), None);
7880    }
7881
7882    #[test]
7883    fn test_total_relationship_weight_sums_all_weights() {
7884        let g = GraphStore::new();
7885        g.add_entity(Entity::new("a", "N")).unwrap();
7886        g.add_entity(Entity::new("b", "N")).unwrap();
7887        g.add_entity(Entity::new("c", "N")).unwrap();
7888        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7889        g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
7890        assert_eq!(g.total_relationship_weight().unwrap(), 4.0);
7891    }
7892
7893    #[test]
7894    fn test_total_relationship_weight_zero_for_no_edges() {
7895        let g = GraphStore::new();
7896        assert_eq!(g.total_relationship_weight().unwrap(), 0.0);
7897    }
7898
7899    // ── Round 57: avg_weight_for_kind, entity_degree_ratio ────────────────────
7900
7901    #[test]
7902    fn test_avg_weight_for_kind_returns_mean() {
7903        let g = GraphStore::new();
7904        g.add_entity(Entity::new("a", "N")).unwrap();
7905        g.add_entity(Entity::new("b", "N")).unwrap();
7906        g.add_entity(Entity::new("c", "N")).unwrap();
7907        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7908        g.add_relationship(Relationship::new("b", "c", "KNOWS", 3.0)).unwrap();
7909        g.add_relationship(Relationship::new("a", "c", "LIKES", 5.0)).unwrap();
7910        assert_eq!(g.avg_weight_for_kind("KNOWS").unwrap(), 2.0);
7911    }
7912
7913    #[test]
7914    fn test_avg_weight_for_kind_zero_for_absent_kind() {
7915        let g = GraphStore::new();
7916        g.add_entity(Entity::new("a", "N")).unwrap();
7917        g.add_entity(Entity::new("b", "N")).unwrap();
7918        g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7919        assert_eq!(g.avg_weight_for_kind("MISSING").unwrap(), 0.0);
7920    }
7921
7922    #[test]
7923    fn test_entity_degree_ratio_returns_fraction_of_total() {
7924        let g = GraphStore::new();
7925        g.add_entity(Entity::new("a", "N")).unwrap();
7926        g.add_entity(Entity::new("b", "N")).unwrap();
7927        g.add_entity(Entity::new("c", "N")).unwrap();
7928        g.add_entity(Entity::new("d", "N")).unwrap();
7929        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7930        g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
7931        let a_id = EntityId("a".to_string());
7932        assert_eq!(g.entity_degree_ratio(&a_id).unwrap(), 0.5);
7933    }
7934
7935    #[test]
7936    fn test_entity_degree_ratio_zero_for_empty_graph() {
7937        let g = GraphStore::new();
7938        let id = EntityId("x".to_string());
7939        assert_eq!(g.entity_degree_ratio(&id).unwrap(), 0.0);
7940    }
7941
7942    // ── Round 57 (extra): entity_has_outgoing_edge, count_nodes_with_self_loop ─
7943
7944    #[test]
7945    fn test_entity_has_outgoing_edge_true_when_edge_exists() {
7946        let g = GraphStore::new();
7947        g.add_entity(Entity::new("a", "N")).unwrap();
7948        g.add_entity(Entity::new("b", "N")).unwrap();
7949        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7950        let id = EntityId("a".to_string());
7951        assert!(g.entity_has_outgoing_edge(&id).unwrap());
7952    }
7953
7954    #[test]
7955    fn test_entity_has_outgoing_edge_false_for_sink_node() {
7956        let g = GraphStore::new();
7957        g.add_entity(Entity::new("a", "N")).unwrap();
7958        g.add_entity(Entity::new("b", "N")).unwrap();
7959        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7960        let id = EntityId("b".to_string());
7961        assert!(!g.entity_has_outgoing_edge(&id).unwrap());
7962    }
7963
7964    #[test]
7965    fn test_count_nodes_with_self_loop_counts_correctly() {
7966        let g = GraphStore::new();
7967        g.add_entity(Entity::new("a", "N")).unwrap();
7968        g.add_entity(Entity::new("b", "N")).unwrap();
7969        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
7970        g.add_relationship(Relationship::new("b", "a", "fwd", 1.0)).unwrap();
7971        assert_eq!(g.count_nodes_with_self_loop().unwrap(), 1);
7972    }
7973
7974    #[test]
7975    fn test_count_nodes_with_self_loop_zero_for_no_loops() {
7976        let g = GraphStore::new();
7977        g.add_entity(Entity::new("a", "N")).unwrap();
7978        g.add_entity(Entity::new("b", "N")).unwrap();
7979        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7980        assert_eq!(g.count_nodes_with_self_loop().unwrap(), 0);
7981    }
7982
7983    // ── Round 51 ──────────────────────────────────────────────────────────────
7984
7985    #[test]
7986    fn test_relationship_count_for_entity_correct() {
7987        let g = GraphStore::new();
7988        g.add_entity(Entity::new("hub", "N")).unwrap();
7989        g.add_entity(Entity::new("a", "N")).unwrap();
7990        g.add_entity(Entity::new("b", "N")).unwrap();
7991        g.add_relationship(Relationship::new("hub", "a", "E", 1.0)).unwrap();
7992        g.add_relationship(Relationship::new("hub", "b", "E", 1.0)).unwrap();
7993        let hub_id = EntityId("hub".to_string());
7994        assert_eq!(g.relationship_count_for_entity(&hub_id).unwrap(), 2);
7995    }
7996
7997    #[test]
7998    fn test_relationship_count_for_entity_zero_for_unknown() {
7999        let g = GraphStore::new();
8000        let id = EntityId("nobody".to_string());
8001        assert_eq!(g.relationship_count_for_entity(&id).unwrap(), 0);
8002    }
8003
8004    // ── Round 59: entities_by_label, edge_count_between ──────────────────────
8005
8006    #[test]
8007    fn test_entities_by_label_returns_matching_ids() {
8008        let g = GraphStore::new();
8009        g.add_entity(Entity::new("a1", "Person")).unwrap();
8010        g.add_entity(Entity::new("a2", "Person")).unwrap();
8011        g.add_entity(Entity::new("a3", "Place")).unwrap();
8012        let mut ids = g.entities_by_label("Person").unwrap();
8013        ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
8014        assert_eq!(ids.len(), 2);
8015        assert_eq!(ids[0].as_str(), "a1");
8016        assert_eq!(ids[1].as_str(), "a2");
8017    }
8018
8019    #[test]
8020    fn test_entities_by_label_empty_for_unknown_label() {
8021        let g = GraphStore::new();
8022        g.add_entity(Entity::new("x", "Thing")).unwrap();
8023        let ids = g.entities_by_label("Nonexistent").unwrap();
8024        assert!(ids.is_empty());
8025    }
8026
8027    #[test]
8028    fn test_edge_count_between_returns_count() {
8029        let g = GraphStore::new();
8030        g.add_entity(Entity::new("u", "N")).unwrap();
8031        g.add_entity(Entity::new("v", "N")).unwrap();
8032        g.add_relationship(Relationship::new("u", "v", "LINKS", 1.0)).unwrap();
8033        g.add_relationship(Relationship::new("u", "v", "ALSO", 1.0)).unwrap();
8034        let from = EntityId::new("u");
8035        let to = EntityId::new("v");
8036        assert_eq!(g.edge_count_between(&from, &to).unwrap(), 2);
8037    }
8038
8039    #[test]
8040    fn test_edge_count_between_zero_when_no_edge() {
8041        let g = GraphStore::new();
8042        g.add_entity(Entity::new("u2", "N")).unwrap();
8043        g.add_entity(Entity::new("v2", "N")).unwrap();
8044        let from = EntityId::new("u2");
8045        let to = EntityId::new("v2");
8046        assert_eq!(g.edge_count_between(&from, &to).unwrap(), 0);
8047    }
8048
8049    // ── Round 60: is_connected ────────────────────────────────────────────────
8050
8051    #[test]
8052    fn test_is_connected_true_when_edge_exists() {
8053        let g = GraphStore::new();
8054        g.add_entity(Entity::new("p", "N")).unwrap();
8055        g.add_entity(Entity::new("q", "N")).unwrap();
8056        g.add_relationship(Relationship::new("p", "q", "LINKS", 1.0)).unwrap();
8057        let p = EntityId::new("p");
8058        let q = EntityId::new("q");
8059        assert!(g.is_connected(&p, &q).unwrap());
8060    }
8061
8062    #[test]
8063    fn test_is_connected_false_when_no_edge() {
8064        let g = GraphStore::new();
8065        g.add_entity(Entity::new("p2", "N")).unwrap();
8066        g.add_entity(Entity::new("q2", "N")).unwrap();
8067        let p2 = EntityId::new("p2");
8068        let q2 = EntityId::new("q2");
8069        assert!(!g.is_connected(&p2, &q2).unwrap());
8070    }
8071
8072    // ── Round 61: graph_is_empty ──────────────────────────────────────────────
8073
8074    #[test]
8075    fn test_graph_is_empty_true_for_new_store() {
8076        let g = GraphStore::new();
8077        assert!(g.graph_is_empty().unwrap());
8078    }
8079
8080    #[test]
8081    fn test_graph_is_empty_false_after_adding_entity() {
8082        let g = GraphStore::new();
8083        g.add_entity(Entity::new("e", "N")).unwrap();
8084        assert!(!g.graph_is_empty().unwrap());
8085    }
8086
8087    // ── Round 62: unique_relationship_kinds, has_any_relationships ────────────
8088
8089    #[test]
8090    fn test_unique_relationship_kinds_returns_sorted_kinds() {
8091        let g = GraphStore::new();
8092        g.add_entity(Entity::new("x", "N")).unwrap();
8093        g.add_entity(Entity::new("y", "N")).unwrap();
8094        g.add_entity(Entity::new("z", "N")).unwrap();
8095        g.add_relationship(Relationship::new("x", "y", "ZEBRA", 1.0)).unwrap();
8096        g.add_relationship(Relationship::new("x", "z", "APPLE", 1.0)).unwrap();
8097        g.add_relationship(Relationship::new("y", "z", "APPLE", 1.0)).unwrap();
8098        let kinds = g.unique_relationship_kinds().unwrap();
8099        assert_eq!(kinds, vec!["APPLE".to_string(), "ZEBRA".to_string()]);
8100    }
8101
8102    #[test]
8103    fn test_unique_relationship_kinds_empty_for_new_graph() {
8104        let g = GraphStore::new();
8105        assert!(g.unique_relationship_kinds().unwrap().is_empty());
8106    }
8107
8108    #[test]
8109    fn test_has_any_relationships_true_when_edge_added() {
8110        let g = GraphStore::new();
8111        g.add_entity(Entity::new("a", "N")).unwrap();
8112        g.add_entity(Entity::new("b", "N")).unwrap();
8113        g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
8114        assert!(g.has_any_relationships().unwrap());
8115    }
8116
8117    #[test]
8118    fn test_has_any_relationships_false_for_new_graph() {
8119        let g = GraphStore::new();
8120        assert!(!g.has_any_relationships().unwrap());
8121    }
8122
8123    // ── Round 63: avg_edge_weight ─────────────────────────────────────────────
8124
8125    #[test]
8126    fn test_avg_edge_weight_correct() {
8127        let g = GraphStore::new();
8128        g.add_entity(Entity::new("n1", "N")).unwrap();
8129        g.add_entity(Entity::new("n2", "N")).unwrap();
8130        g.add_relationship(Relationship::new("n1", "n2", "E", 2.0)).unwrap();
8131        g.add_relationship(Relationship::new("n1", "n2", "F", 4.0)).unwrap();
8132        assert!((g.avg_edge_weight().unwrap() - 3.0).abs() < 1e-6);
8133    }
8134
8135    #[test]
8136    fn test_avg_edge_weight_zero_for_empty_graph() {
8137        let g = GraphStore::new();
8138        assert_eq!(g.avg_edge_weight().unwrap(), 0.0);
8139    }
8140
8141    // ── Round 58: nodes_with_no_outgoing ──────────────────────────────────────
8142
8143    #[test]
8144    fn test_nodes_with_no_outgoing_returns_sink_nodes() {
8145        let g = GraphStore::new();
8146        g.add_entity(Entity::new("src", "N")).unwrap();
8147        g.add_entity(Entity::new("sink", "N")).unwrap();
8148        g.add_entity(Entity::new("iso", "N")).unwrap();
8149        g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
8150        let sinks = g.nodes_with_no_outgoing().unwrap();
8151        let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
8152        assert!(ids.contains(&"sink"));
8153        assert!(ids.contains(&"iso"));
8154        assert!(!ids.contains(&"src"));
8155    }
8156
8157    #[test]
8158    fn test_nodes_with_no_outgoing_all_for_graph_with_no_edges() {
8159        let g = GraphStore::new();
8160        g.add_entity(Entity::new("a", "N")).unwrap();
8161        g.add_entity(Entity::new("b", "N")).unwrap();
8162        assert_eq!(g.nodes_with_no_outgoing().unwrap().len(), 2);
8163    }
8164
8165    // ── Round 58: relationship_kinds, max_out_degree ──────────────────────────
8166
8167    #[test]
8168    fn test_relationship_kinds_returns_sorted_unique_kinds() {
8169        let g = GraphStore::new();
8170        g.add_entity(Entity::new("a", "N")).unwrap();
8171        g.add_entity(Entity::new("b", "N")).unwrap();
8172        g.add_entity(Entity::new("c", "N")).unwrap();
8173        g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
8174        g.add_relationship(Relationship::new("b", "c", "likes", 1.0)).unwrap();
8175        g.add_relationship(Relationship::new("a", "c", "follows", 1.0)).unwrap();
8176        let kinds = g.relationship_kinds().unwrap();
8177        assert_eq!(kinds, vec!["follows", "likes"]);
8178    }
8179
8180    #[test]
8181    fn test_relationship_kinds_empty_for_empty_graph() {
8182        let g = GraphStore::new();
8183        assert!(g.relationship_kinds().unwrap().is_empty());
8184    }
8185
8186    #[test]
8187    fn test_max_out_degree_returns_correct_max() {
8188        let g = GraphStore::new();
8189        g.add_entity(Entity::new("a", "N")).unwrap();
8190        g.add_entity(Entity::new("b", "N")).unwrap();
8191        g.add_entity(Entity::new("c", "N")).unwrap();
8192        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
8193        g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
8194        g.add_relationship(Relationship::new("b", "c", "k", 1.0)).unwrap();
8195        assert_eq!(g.max_out_degree().unwrap(), 2);
8196    }
8197
8198    // ── Round 59: in_degree_of ─────────────────────────────────────────────────
8199
8200    #[test]
8201    fn test_in_degree_of_counts_incoming_edges() {
8202        let g = GraphStore::new();
8203        g.add_entity(Entity::new("src1", "N")).unwrap();
8204        g.add_entity(Entity::new("src2", "N")).unwrap();
8205        g.add_entity(Entity::new("dst", "N")).unwrap();
8206        g.add_relationship(Relationship::new("src1", "dst", "k", 1.0)).unwrap();
8207        g.add_relationship(Relationship::new("src2", "dst", "k", 1.0)).unwrap();
8208        let dst = EntityId("dst".to_string());
8209        assert_eq!(g.in_degree_of(&dst).unwrap(), 2);
8210    }
8211
8212    #[test]
8213    fn test_in_degree_of_zero_for_node_with_no_incoming() {
8214        let g = GraphStore::new();
8215        g.add_entity(Entity::new("alone", "N")).unwrap();
8216        let id = EntityId("alone".to_string());
8217        assert_eq!(g.in_degree_of(&id).unwrap(), 0);
8218    }
8219
8220    // ── Round 60: total_weight_for_kind, entity_ids_with_label ────────────────
8221
8222    #[test]
8223    fn test_total_weight_for_kind_sums_correctly() {
8224        let g = GraphStore::new();
8225        g.add_entity(Entity::new("a", "N")).unwrap();
8226        g.add_entity(Entity::new("b", "N")).unwrap();
8227        g.add_entity(Entity::new("c", "N")).unwrap();
8228        g.add_relationship(Relationship::new("a", "b", "link", 2.0)).unwrap();
8229        g.add_relationship(Relationship::new("b", "c", "link", 4.0)).unwrap();
8230        g.add_relationship(Relationship::new("a", "c", "other", 1.0)).unwrap();
8231        let sum = g.total_weight_for_kind("link").unwrap();
8232        assert!((sum - 6.0).abs() < 1e-9);
8233    }
8234
8235    #[test]
8236    fn test_total_weight_for_kind_zero_for_unknown_kind() {
8237        let g = GraphStore::new();
8238        assert_eq!(g.total_weight_for_kind("nope").unwrap(), 0.0);
8239    }
8240
8241    #[test]
8242    fn test_entity_ids_with_label_returns_matching_ids() {
8243        let g = GraphStore::new();
8244        g.add_entity(Entity::new("e1", "Person")).unwrap();
8245        g.add_entity(Entity::new("e2", "Person")).unwrap();
8246        g.add_entity(Entity::new("e3", "Place")).unwrap();
8247        let mut ids = g.entity_ids_with_label("Person").unwrap();
8248        ids.sort_by(|a, b| a.0.cmp(&b.0));
8249        let strs: Vec<&str> = ids.iter().map(|id| id.0.as_str()).collect();
8250        assert_eq!(strs, vec!["e1", "e2"]);
8251    }
8252
8253    #[test]
8254    fn test_entity_ids_with_label_empty_for_unknown_label() {
8255        let g = GraphStore::new();
8256        assert!(g.entity_ids_with_label("Unknown").unwrap().is_empty());
8257    }
8258
8259    // ── Round 59: out_degree_of, has_relationship_with_kind ───────────────────
8260
8261    #[test]
8262    fn test_out_degree_of_returns_edge_count() {
8263        let g = GraphStore::new();
8264        g.add_entity(Entity::new("a", "N")).unwrap();
8265        g.add_entity(Entity::new("b", "N")).unwrap();
8266        g.add_entity(Entity::new("c", "N")).unwrap();
8267        g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
8268        g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
8269        let id = EntityId("a".to_string());
8270        assert_eq!(g.out_degree_of(&id).unwrap(), 2);
8271    }
8272
8273    #[test]
8274    fn test_out_degree_of_zero_for_sink_node() {
8275        let g = GraphStore::new();
8276        g.add_entity(Entity::new("sink", "N")).unwrap();
8277        let id = EntityId("sink".to_string());
8278        assert_eq!(g.out_degree_of(&id).unwrap(), 0);
8279    }
8280
8281    #[test]
8282    fn test_has_relationship_with_kind_true_when_kind_present() {
8283        let g = GraphStore::new();
8284        g.add_entity(Entity::new("a", "N")).unwrap();
8285        g.add_entity(Entity::new("b", "N")).unwrap();
8286        g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
8287        assert!(g.has_relationship_with_kind("follows").unwrap());
8288    }
8289
8290    #[test]
8291    fn test_has_relationship_with_kind_false_for_absent_kind() {
8292        let g = GraphStore::new();
8293        g.add_entity(Entity::new("a", "N")).unwrap();
8294        g.add_entity(Entity::new("b", "N")).unwrap();
8295        g.add_relationship(Relationship::new("a", "b", "likes", 1.0)).unwrap();
8296        assert!(!g.has_relationship_with_kind("follows").unwrap());
8297    }
8298
8299    // ── Round 61: labels_unique_count, relationships_from ─────────────────────
8300
8301    #[test]
8302    fn test_labels_unique_count_returns_distinct_count() {
8303        let g = GraphStore::new();
8304        g.add_entity(Entity::new("e1", "Person")).unwrap();
8305        g.add_entity(Entity::new("e2", "Person")).unwrap();
8306        g.add_entity(Entity::new("e3", "Place")).unwrap();
8307        assert_eq!(g.labels_unique_count().unwrap(), 2);
8308    }
8309
8310    #[test]
8311    fn test_labels_unique_count_zero_for_empty_graph() {
8312        let g = GraphStore::new();
8313        assert_eq!(g.labels_unique_count().unwrap(), 0);
8314    }
8315
8316    #[test]
8317    fn test_relationships_from_returns_outgoing_edges() {
8318        let g = GraphStore::new();
8319        g.add_entity(Entity::new("a", "N")).unwrap();
8320        g.add_entity(Entity::new("b", "N")).unwrap();
8321        g.add_entity(Entity::new("c", "N")).unwrap();
8322        g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
8323        g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
8324        let from = EntityId("a".to_string());
8325        assert_eq!(g.relationships_from(&from).unwrap().len(), 2);
8326    }
8327
8328    #[test]
8329    fn test_relationships_from_empty_for_node_with_no_edges() {
8330        let g = GraphStore::new();
8331        g.add_entity(Entity::new("lone", "N")).unwrap();
8332        let id = EntityId("lone".to_string());
8333        assert!(g.relationships_from(&id).unwrap().is_empty());
8334    }
8335
8336    // ── Round 62: cycle_count ─────────────────────────────────────────────────
8337
8338    #[test]
8339    fn test_cycle_count_counts_self_loops() {
8340        let g = GraphStore::new();
8341        g.add_entity(Entity::new("a", "N")).unwrap();
8342        g.add_entity(Entity::new("b", "N")).unwrap();
8343        g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
8344        g.add_relationship(Relationship::new("b", "b", "self", 1.0)).unwrap();
8345        g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
8346        assert_eq!(g.cycle_count().unwrap(), 2);
8347    }
8348
8349    #[test]
8350    fn test_cycle_count_zero_when_no_self_loops() {
8351        let g = GraphStore::new();
8352        g.add_entity(Entity::new("x", "N")).unwrap();
8353        g.add_entity(Entity::new("y", "N")).unwrap();
8354        g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
8355        assert_eq!(g.cycle_count().unwrap(), 0);
8356    }
8357
8358    // ── Round 63: entities_with_property_key, entity_properties_count ────────
8359
8360    #[test]
8361    fn test_entities_with_property_key_returns_matching_entities() {
8362        let g = GraphStore::new();
8363        let e1 = Entity::new("e1", "N").with_property("color", serde_json::json!("red"));
8364        let e2 = Entity::new("e2", "N");
8365        g.add_entity(e1).unwrap();
8366        g.add_entity(e2).unwrap();
8367        let result = g.entities_with_property_key("color").unwrap();
8368        assert_eq!(result.len(), 1);
8369        assert_eq!(result[0].id.0, "e1");
8370    }
8371
8372    #[test]
8373    fn test_entity_properties_count_returns_count_of_matching_entities() {
8374        let g = GraphStore::new();
8375        let e1 = Entity::new("p1", "N").with_property("tag", serde_json::json!("x"));
8376        let e2 = Entity::new("p2", "N").with_property("tag", serde_json::json!("y"));
8377        let e3 = Entity::new("p3", "N");
8378        g.add_entity(e1).unwrap();
8379        g.add_entity(e2).unwrap();
8380        g.add_entity(e3).unwrap();
8381        assert_eq!(g.entity_properties_count("tag").unwrap(), 2);
8382    }
8383
8384    // ── Round 63: all_entities_have_properties ────────────────────────────────
8385
8386    #[test]
8387    fn test_all_entities_have_properties_true_when_all_have_props() {
8388        let g = GraphStore::new();
8389        let e1 = Entity::new("a", "N").with_property("k", "v".into());
8390        let e2 = Entity::new("b", "N").with_property("k", "v".into());
8391        g.add_entity(e1).unwrap();
8392        g.add_entity(e2).unwrap();
8393        assert!(g.all_entities_have_properties().unwrap());
8394    }
8395
8396    #[test]
8397    fn test_all_entities_have_properties_false_when_one_has_none() {
8398        let g = GraphStore::new();
8399        let e1 = Entity::new("a", "N").with_property("k", "v".into());
8400        let e2 = Entity::new("b", "N");
8401        g.add_entity(e1).unwrap();
8402        g.add_entity(e2).unwrap();
8403        assert!(!g.all_entities_have_properties().unwrap());
8404    }
8405
8406    #[test]
8407    fn test_all_entities_have_properties_true_for_empty_graph() {
8408        let g = GraphStore::new();
8409        assert!(g.all_entities_have_properties().unwrap());
8410    }
8411
8412    // ── Round 64: edges_to, entity_has_property_value ────────────────────────
8413
8414    #[test]
8415    fn test_edges_to_returns_incoming_relationships() {
8416        let g = GraphStore::new();
8417        g.add_entity(Entity::new("a", "N")).unwrap();
8418        g.add_entity(Entity::new("b", "N")).unwrap();
8419        g.add_entity(Entity::new("c", "N")).unwrap();
8420        g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
8421        g.add_relationship(Relationship::new("b", "c", "EDGE", 1.0)).unwrap();
8422        let incoming = g.edges_to(&EntityId("c".into())).unwrap();
8423        assert_eq!(incoming.len(), 2);
8424    }
8425
8426    #[test]
8427    fn test_edges_to_empty_for_no_incoming() {
8428        let g = GraphStore::new();
8429        g.add_entity(Entity::new("x", "N")).unwrap();
8430        assert!(g.edges_to(&EntityId("x".into())).unwrap().is_empty());
8431    }
8432
8433    #[test]
8434    fn test_entity_has_property_value_true_when_matches() {
8435        let g = GraphStore::new();
8436        g.add_entity(Entity::new("e1", "N").with_property("color", serde_json::json!("blue"))).unwrap();
8437        assert!(g.entity_has_property_value(&EntityId("e1".into()), "color", "blue").unwrap());
8438    }
8439
8440    #[test]
8441    fn test_entity_has_property_value_false_for_unknown_entity() {
8442        let g = GraphStore::new();
8443        assert!(!g.entity_has_property_value(&EntityId("nope".into()), "color", "blue").unwrap());
8444    }
8445}