Skip to main content

hirn_engine/graph/
graph_store.rs

1//! F-003 FIX: Unified async graph store trait.
2//!
3//! Defines [`GraphStore`], the common interface for graph backends.
4//! Two implementations ship with hirn-engine:
5//!
6//! | Backend | Backing store | Scaling |
7//! |---------|--------------|---------|
8//! | [`PersistentGraph`](crate::PersistentGraph) | LanceDB datasets | Disk-backed, horizontal |
9//! | In-memory `PropertyGraph` | petgraph `StableDiGraph` | RAM-limited |
10//!
11//! When a `PersistentGraph` is configured, it becomes the **primary** graph
12//! store. The in-memory `PropertyGraph` is retained only as an optional
13//! hot-path cache for algorithms that require synchronous traversal
14//! (spreading activation, community detection, Hebbian co-firing).
15//!
16//! ## Migration path
17//!
18//! Code that previously used the dual-dispatch pattern:
19//! ```rust,ignore
20//! if let Some(pg) = &self.persistent_graph {
21//!     pg.add_edge(source, target, relation, weight, meta).await
22//! } else {
23//!     let mut graph = self.graph.write();
24//!     graph.add_edge(source, target, relation, weight, meta)
25//! }
26//! ```
27//!
28//! Should migrate to the unified trait:
29//! ```rust,ignore
30//! self.graph_store().add_edge(source, target, relation, weight, meta).await
31//! ```
32
33use std::collections::HashMap;
34
35use async_trait::async_trait;
36
37use hirn_core::HirnResult;
38use hirn_core::id::MemoryId;
39use hirn_core::metadata::Metadata;
40use hirn_core::timestamp::Timestamp;
41use hirn_core::types::{EdgeRelation, Layer, Namespace};
42
43use crate::graph::{CausalEdgeData, EdgeId, GraphEdge, GraphNodeData};
44
45/// Unified async interface for graph storage backends.
46///
47/// Both the in-memory `PropertyGraph` and the LanceDB-backed
48/// `PersistentGraph` implement this trait, enabling code to operate on either
49/// backend without branching.
50///
51/// All methods are async to accommodate the `PersistentGraph` path. The
52/// in-memory implementation wraps synchronous operations.
53#[async_trait]
54pub trait GraphStore: Send + Sync {
55    // ── Node operations ─────────────────────────────────────────────────
56
57    /// Insert a graph node. Returns `true` if newly inserted, `false` if it
58    /// already existed.
59    async fn add_node(
60        &self,
61        id: MemoryId,
62        layer: Layer,
63        importance: f32,
64        created_at: Timestamp,
65        namespace: Namespace,
66    ) -> HirnResult<bool>;
67
68    /// Remove a node and all its incident edges. Returns `true` if the node
69    /// existed.
70    async fn remove_node(&self, id: MemoryId) -> HirnResult<bool>;
71
72    /// Check whether a node exists.
73    async fn has_node(&self, id: MemoryId) -> HirnResult<bool>;
74
75    /// Retrieve full node data, or `None` if absent.
76    async fn get_node(&self, id: MemoryId) -> HirnResult<Option<GraphNodeData>>;
77
78    /// Return all node IDs in the graph.
79    async fn node_ids(&self) -> HirnResult<Vec<MemoryId>>;
80
81    /// Get the importance score for a node.
82    async fn node_importance(&self, id: MemoryId) -> HirnResult<Option<f32>>;
83
84    /// Set the importance score for a node.
85    async fn set_node_importance(&self, id: MemoryId, importance: f32) -> HirnResult<()>;
86
87    /// Get the layer of a node.
88    async fn node_layer(&self, id: MemoryId) -> HirnResult<Option<Layer>>;
89
90    /// Get the namespace of a node.
91    async fn node_namespace(&self, id: MemoryId) -> HirnResult<Option<Namespace>>;
92
93    /// Check whether two nodes' namespaces are compatible for auto-edge
94    /// creation (same namespace, or either is "shared").
95    async fn namespaces_compatible(&self, a: MemoryId, b: MemoryId) -> HirnResult<bool>;
96
97    // ── Edge operations ─────────────────────────────────────────────────
98
99    /// Create a directed edge. Returns the new [`EdgeId`].
100    ///
101    /// Implementations should enforce the per-node fan-out cap
102    /// (`MAX_EDGES_PER_NODE`).
103    async fn add_edge(
104        &self,
105        source: MemoryId,
106        target: MemoryId,
107        relation: EdgeRelation,
108        weight: f32,
109        metadata: Metadata,
110    ) -> HirnResult<EdgeId>;
111
112    /// Create a causal edge with associated [`CausalEdgeData`].
113    ///
114    /// Identical to [`add_edge`] but populates strength, confidence,
115    /// evidence count, and mechanism on the created edge.
116    async fn add_causal_edge(
117        &self,
118        source: MemoryId,
119        target: MemoryId,
120        relation: EdgeRelation,
121        weight: f32,
122        metadata: Metadata,
123        causal: CausalEdgeData,
124    ) -> HirnResult<EdgeId>;
125
126    /// Remove an edge by ID.
127    async fn remove_edge(&self, edge_id: EdgeId) -> HirnResult<()>;
128
129    /// Get a single edge by ID.
130    async fn get_edge(&self, edge_id: EdgeId) -> HirnResult<Option<GraphEdge>>;
131
132    /// Get all edges incident to a node (both directions).
133    async fn get_edges(&self, node_id: MemoryId) -> HirnResult<Vec<GraphEdge>>;
134
135    /// Get edges between two specific nodes.
136    async fn get_edges_between(&self, a: MemoryId, b: MemoryId) -> HirnResult<Vec<GraphEdge>>;
137
138    /// Get edges of a specific relation type incident to a node.
139    async fn get_edges_of_type(
140        &self,
141        node_id: MemoryId,
142        relation: EdgeRelation,
143    ) -> HirnResult<Vec<GraphEdge>>;
144
145    /// Get edges of a specific relation type incident to many nodes.
146    async fn get_edges_of_type_many(
147        &self,
148        node_ids: &[MemoryId],
149        relation: EdgeRelation,
150    ) -> HirnResult<HashMap<MemoryId, Vec<GraphEdge>>> {
151        let mut result = HashMap::with_capacity(node_ids.len());
152        for &node_id in node_ids {
153            let edges = self.get_edges_of_type(node_id, relation).await?;
154            if !edges.is_empty() {
155                result.insert(node_id, edges);
156            }
157        }
158        Ok(result)
159    }
160
161    /// Get all edges in the graph.
162    async fn all_edges(&self) -> HirnResult<Vec<GraphEdge>>;
163
164    /// Update the weight (and optionally co-retrieval count) of an edge.
165    async fn update_edge_weight(
166        &self,
167        edge_id: EdgeId,
168        new_weight: f32,
169        co_retrieval_count: Option<u64>,
170    ) -> HirnResult<()>;
171
172    // ── Traversal ───────────────────────────────────────────────────────
173
174    /// BFS neighbors up to `depth` hops, filtering by minimum edge weight.
175    async fn get_neighbors(
176        &self,
177        start: MemoryId,
178        depth: usize,
179        min_weight: f32,
180    ) -> HirnResult<Vec<MemoryId>>;
181
182    /// BFS neighbors with optional namespace filter.
183    async fn get_neighbors_filtered(
184        &self,
185        start: MemoryId,
186        depth: usize,
187        min_weight: f32,
188        namespace: Option<&Namespace>,
189    ) -> HirnResult<Vec<MemoryId>>;
190
191    /// Outgoing edges with `(target, weight, relation)` tuples.
192    async fn outgoing_weighted(
193        &self,
194        node_id: MemoryId,
195    ) -> HirnResult<Vec<(MemoryId, f32, EdgeRelation)>>;
196
197    /// Shortest path between two nodes (Dijkstra). Returns `None` if no
198    /// path exists.
199    async fn shortest_path(
200        &self,
201        source: MemoryId,
202        target: MemoryId,
203    ) -> HirnResult<Option<Vec<MemoryId>>>;
204
205    // ── Counts ──────────────────────────────────────────────────────────
206
207    /// Number of nodes in the graph.
208    async fn node_count(&self) -> HirnResult<usize>;
209
210    /// Number of edges in the graph.
211    async fn edge_count(&self) -> HirnResult<usize>;
212}