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}