1use std::fmt::Write as _;
8
9use surrealdb::Surreal;
10
11use super::confidence;
12use super::error::GraphError;
13use super::store::Db;
14use super::types::*;
15
16pub async fn traverse(
19 db: &Surreal<Db>,
20 entity_name: &str,
21 max_depth: u32,
22) -> Result<TraversalNode, GraphError> {
23 traverse_filtered(db, entity_name, max_depth, None).await
24}
25
26pub async fn traverse_filtered(
29 db: &Surreal<Db>,
30 entity_name: &str,
31 max_depth: u32,
32 type_filter: Option<&str>,
33) -> Result<TraversalNode, GraphError> {
34 let full = super::crud::get_entity_by_name(db, entity_name)
36 .await?
37 .ok_or_else(|| GraphError::NotFound(entity_name.to_string()))?;
38
39 super::crud::increment_access_counts(db, &[full.id_string()]).await?;
41
42 let root = EntitySummary {
43 id: full.id.clone(),
44 name: full.name,
45 entity_type: full.entity_type,
46 abstract_text: full.abstract_text,
47 };
48
49 traverse_from(db, &root, max_depth, 0, &mut vec![], type_filter).await
50}
51
52type TraversalFuture<'a> = std::pin::Pin<
54 Box<dyn std::future::Future<Output = Result<TraversalNode, GraphError>> + Send + 'a>,
55>;
56
57fn traverse_from<'a>(
59 db: &'a Surreal<Db>,
60 entity: &'a EntitySummary,
61 max_depth: u32,
62 current_depth: u32,
63 visited: &'a mut Vec<String>,
64 type_filter: Option<&'a str>,
65) -> TraversalFuture<'a> {
66 Box::pin(async move {
67 visited.push(entity.id_string());
68
69 if current_depth >= max_depth {
70 return Ok(TraversalNode {
71 entity: entity.clone(),
72 edges: vec![],
73 });
74 }
75
76 let mut edges = Vec::new();
77
78 let now = chrono::Utc::now();
79
80 let mut response = db
82 .query(
83 r#"
84 SELECT
85 rel_type,
86 valid_from,
87 valid_until,
88 confidence,
89 last_reinforced,
90 out AS target_id
91 FROM relates_to
92 WHERE in = type::record($id)
93 AND valid_until IS NONE
94 "#,
95 )
96 .bind(("id", entity.id_string()))
97 .await?;
98
99 let outgoing: Vec<EdgeRow> = super::deserialize_take(&mut response, 0)?;
100 collect_edges(
101 db,
102 outgoing,
103 "->",
104 max_depth,
105 current_depth,
106 visited,
107 type_filter,
108 &mut edges,
109 &now,
110 )
111 .await?;
112
113 let mut response = db
115 .query(
116 r#"
117 SELECT
118 rel_type,
119 valid_from,
120 valid_until,
121 confidence,
122 last_reinforced,
123 in AS target_id
124 FROM relates_to
125 WHERE out = type::record($id)
126 AND valid_until IS NONE
127 "#,
128 )
129 .bind(("id", entity.id_string()))
130 .await?;
131
132 let incoming: Vec<EdgeRow> = super::deserialize_take(&mut response, 0)?;
133 collect_edges(
134 db,
135 incoming,
136 "<-",
137 max_depth,
138 current_depth,
139 visited,
140 type_filter,
141 &mut edges,
142 &now,
143 )
144 .await?;
145
146 Ok(TraversalNode {
147 entity: entity.clone(),
148 edges,
149 })
150 })
151}
152
153#[allow(clippy::too_many_arguments)]
155async fn collect_edges<'a>(
156 db: &'a Surreal<Db>,
157 edge_rows: Vec<EdgeRow>,
158 direction: &str,
159 max_depth: u32,
160 current_depth: u32,
161 visited: &'a mut Vec<String>,
162 type_filter: Option<&'a str>,
163 edges: &'a mut Vec<TraversalEdge>,
164 now: &'a chrono::DateTime<chrono::Utc>,
165) -> Result<(), GraphError> {
166 for edge in edge_rows {
167 let effective = confidence::effective_confidence(
169 edge.confidence,
170 edge.last_reinforced.as_ref(),
171 &edge.valid_from,
172 now,
173 );
174
175 if effective < 0.1 {
177 continue;
178 }
179
180 let tid = edge.target_id_string();
181
182 let target: Option<EntitySummary> = super::crud::get_entity_summary(db, &tid).await?;
184 if let Some(target) = target {
185 if visited.contains(&target.id_string()) {
186 continue;
187 }
188
189 if let Some(filter) = type_filter {
191 if target.entity_type.to_string() != filter {
192 continue;
193 }
194 }
195
196 let child = traverse_from(
197 db,
198 &target,
199 max_depth,
200 current_depth + 1,
201 visited,
202 type_filter,
203 )
204 .await?;
205
206 edges.push(TraversalEdge {
207 rel_type: edge.rel_type,
208 direction: direction.to_string(),
209 target: child,
210 valid_from: edge.valid_from,
211 valid_until: edge.valid_until,
212 confidence: effective,
213 });
214 }
215 }
216
217 Ok(())
218}
219
220#[must_use]
222pub fn format_traversal(node: &TraversalNode, indent: usize) -> String {
223 let mut out = String::new();
224 let prefix = " ".repeat(indent);
225
226 if indent == 0 {
227 let _ = writeln!(out, "{} ({})", node.entity.name, node.entity.entity_type);
228 }
229
230 for edge in &node.edges {
231 let superseded = if edge.valid_until.is_some() {
232 " [superseded]"
233 } else {
234 ""
235 };
236
237 let confidence_tag = if edge.confidence < 1.0 {
238 format!(" [{}%]", (edge.confidence * 100.0).round() as u32)
239 } else {
240 String::new()
241 };
242
243 let _ = writeln!(
244 out,
245 "{prefix}├── {} {} {}{confidence_tag}{superseded}",
246 edge.direction, edge.rel_type, edge.target.entity.name,
247 );
248
249 if !edge.target.edges.is_empty() {
250 out.push_str(&format_traversal(&edge.target, indent + 1));
251 }
252 }
253
254 out
255}