1pub mod convex;
2pub mod store;
3pub mod types;
4
5pub use convex::{
6 ConvexEdgeRow, ConvexGraphClient, ConvexGraphStore, ConvexNodeRow, ConvexProjectionRows,
7 ConvexRowsGraphClient,
8};
9pub use store::{
10 GraphStore, apply_graph_edge_query_page, apply_graph_query_page, graph_semantic_cosine,
11 graph_semantic_seeded_edge_other_id, graph_semantic_seeded_edge_score,
12 graph_semantic_top_candidates_by_property_scan, parse_graph_semantic_vector_property,
13 shortest_path_using_outgoing,
14};
15pub use types::{
16 DEFAULT_RANKED_NEIGHBORHOOD_MEMORY_NODE_BOOST,
17 DEFAULT_RANKED_NEIGHBORHOOD_OBSERVED_AT_HALF_LIFE_SECS,
18 DEFAULT_RANKED_NEIGHBORHOOD_OBSERVED_AT_WEIGHT, GRAPH_SEMANTIC_VECTOR_DEFAULT_MODEL,
19 GRAPH_SEMANTIC_VECTOR_MODEL_PROPERTY_KEY, GRAPH_SEMANTIC_VECTOR_PROPERTY_KEY, GraphEdge,
20 GraphFreshness, GraphNode, GraphPagedSubgraph, GraphPath, GraphProjection, GraphPropertyFilter,
21 GraphProvenance, GraphQueryOptions, GraphQueryPage, GraphSemanticCandidate, GraphSubgraph,
22 NeighborhoodScoring, PropertyMode, RankedNeighborhoodOptions, RankedNeighborhoodResult,
23 SQLITE_GRAPH_SCHEMA_VERSION, SemanticSeededNeighborhoodExpansion,
24 SemanticSeededNeighborhoodOptions, SemanticSeededNeighborhoodResult, TerseGraphEdge,
25 TerseGraphNode, TerseGraphSubgraph, TerseHealthScore, TerseSearchHit, graph_edge_id,
26 stable_graph_edge_id,
27};
28
29impl GraphProjection {
30 pub fn upsert_into<S: GraphStore + ?Sized>(&self, store: &S) -> anyhow::Result<()> {
31 for node in &self.nodes {
32 store.upsert_node(node)?;
33 }
34 for edge in &self.edges {
35 store.upsert_edge(edge)?;
36 }
37 Ok(())
38 }
39
40 pub fn to_convex_rows(&self) -> ConvexProjectionRows {
41 ConvexProjectionRows::from(self)
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use std::cell::RefCell;
49 use std::collections::BTreeMap;
50
51 fn sample_provenance() -> GraphProvenance {
52 GraphProvenance::new("fixture", "src/lib.rs:1").with_content_hash("hash-1")
53 }
54
55 fn sample_projection() -> GraphProjection {
56 let source = sample_provenance();
57 GraphProjection {
58 nodes: vec![
59 GraphNode::new("doc:livekit", "document", "LiveKit guide")
60 .with_property("domain", "livekit")
61 .with_provenance(source.clone())
62 .with_freshness(GraphFreshness::content_hash("node-hash")),
63 GraphNode::new("topic:rooms", "topic", "Rooms"),
64 GraphNode::new("topic:egress", "topic", "Egress"),
65 ],
66 edges: vec![
67 GraphEdge::new("doc:livekit", "topic:rooms", "mentions")
68 .with_property("confidence", "0.91")
69 .with_provenance(source.clone())
70 .with_freshness(GraphFreshness::content_hash("edge-hash")),
71 GraphEdge::new("topic:rooms", "topic:egress", "related_to").with_provenance(source),
72 ],
73 }
74 }
75
76 fn assert_projection_store_contract(store: &impl GraphStore) {
77 let projection = sample_projection();
78 projection.upsert_into(store).unwrap();
79
80 assert_eq!(
81 store.node("doc:livekit").unwrap(),
82 projection
83 .nodes
84 .iter()
85 .find(|node| node.id == "doc:livekit")
86 .cloned()
87 );
88 assert_eq!(
89 store.nodes_by_kind("topic").unwrap(),
90 vec![
91 GraphNode::new("topic:egress", "topic", "Egress"),
92 GraphNode::new("topic:rooms", "topic", "Rooms"),
93 ]
94 );
95
96 let mentions = store
97 .outgoing_edges("doc:livekit", Some("mentions"))
98 .unwrap();
99 assert_eq!(mentions.len(), 1);
100 assert_eq!(mentions[0].to_id, "topic:rooms");
101 assert_eq!(
102 mentions[0].properties.get("confidence"),
103 Some(&"0.91".into())
104 );
105
106 let path = store
107 .shortest_path("doc:livekit", "topic:egress", None)
108 .unwrap()
109 .unwrap();
110 assert_eq!(
111 path.nodes,
112 vec!["doc:livekit", "topic:rooms", "topic:egress"]
113 );
114 }
115
116 #[derive(Default)]
117 struct MemoryConvexGraphClient {
118 nodes: RefCell<BTreeMap<String, ConvexNodeRow>>,
119 edges: RefCell<BTreeMap<String, ConvexEdgeRow>>,
120 }
121
122 impl ConvexGraphClient for MemoryConvexGraphClient {
123 fn upsert_node_row(&self, row: &ConvexNodeRow) -> anyhow::Result<()> {
124 self.nodes
125 .borrow_mut()
126 .insert(row.external_id.clone(), row.clone());
127 Ok(())
128 }
129
130 fn upsert_edge_row(&self, row: &ConvexEdgeRow) -> anyhow::Result<()> {
131 self.edges
132 .borrow_mut()
133 .insert(row.edge_key.clone(), row.clone());
134 Ok(())
135 }
136
137 fn delete_node_row(&self, external_id: &str) -> anyhow::Result<usize> {
138 Ok(usize::from(
139 self.nodes.borrow_mut().remove(external_id).is_some(),
140 ))
141 }
142
143 fn delete_edge_row(&self, edge_key: &str) -> anyhow::Result<usize> {
144 Ok(usize::from(
145 self.edges.borrow_mut().remove(edge_key).is_some(),
146 ))
147 }
148
149 fn node_row(&self, external_id: &str) -> anyhow::Result<Option<ConvexNodeRow>> {
150 Ok(self.nodes.borrow().get(external_id).cloned())
151 }
152
153 fn node_rows(&self) -> anyhow::Result<Vec<ConvexNodeRow>> {
154 Ok(self.nodes.borrow().values().cloned().collect())
155 }
156
157 fn edge_rows(&self) -> anyhow::Result<Vec<ConvexEdgeRow>> {
158 Ok(self.edges.borrow().values().cloned().collect())
159 }
160
161 fn node_rows_by_kind(&self, kind: &str) -> anyhow::Result<Vec<ConvexNodeRow>> {
162 Ok(self
163 .nodes
164 .borrow()
165 .values()
166 .filter(|row| row.kind == kind)
167 .cloned()
168 .collect())
169 }
170
171 fn outgoing_edge_rows(
172 &self,
173 from_external_id: &str,
174 kind: Option<&str>,
175 ) -> anyhow::Result<Vec<ConvexEdgeRow>> {
176 Ok(self
177 .edges
178 .borrow()
179 .values()
180 .filter(|row| row.from_external_id == from_external_id)
181 .filter(|row| kind.is_none_or(|kind| row.kind == kind))
182 .cloned()
183 .collect())
184 }
185 }
186
187 #[test]
188 fn graph_projection_round_trips_through_backend_agnostic_store_contract() {
189 let convex = ConvexGraphStore::new(MemoryConvexGraphClient::default());
190 assert_projection_store_contract(&convex);
191
192 let client = convex.client();
193 assert_eq!(client.nodes.borrow().len(), 3);
194 assert_eq!(client.edges.borrow().len(), 2);
195 assert!(
196 client.nodes.borrow().contains_key("doc:livekit"),
197 "Convex rows keep GraphNode.id as the externalId upsert key"
198 );
199 }
200
201 #[test]
202 fn graph_store_contract_covers_crud_neighborhood_and_ordering() {
203 fn assert_crud_contract(store: &impl GraphStore) {
204 let projection = sample_projection();
205 projection.upsert_into(store).unwrap();
206
207 let neighborhood = store.neighborhood("doc:livekit", 2, None).unwrap().unwrap();
208 assert_eq!(
209 neighborhood
210 .nodes
211 .iter()
212 .map(|node| node.id.as_str())
213 .collect::<Vec<_>>(),
214 vec!["doc:livekit", "topic:egress", "topic:rooms"]
215 );
216 assert_eq!(
217 neighborhood
218 .edges
219 .iter()
220 .map(|edge| (
221 edge.from_id.as_str(),
222 edge.kind.as_str(),
223 edge.to_id.as_str()
224 ))
225 .collect::<Vec<_>>(),
226 vec![
227 ("doc:livekit", "mentions", "topic:rooms"),
228 ("topic:rooms", "related_to", "topic:egress"),
229 ]
230 );
231
232 assert_eq!(
233 store
234 .delete_edge("topic:rooms", "topic:egress", "related_to")
235 .unwrap(),
236 1
237 );
238 assert!(
239 store
240 .shortest_path("doc:livekit", "topic:egress", None)
241 .unwrap()
242 .is_none()
243 );
244 assert_eq!(store.delete_node("topic:rooms").unwrap(), 1);
245 assert!(store.node("topic:rooms").unwrap().is_none());
246 assert!(
247 store
248 .outgoing_edges("doc:livekit", None)
249 .unwrap()
250 .is_empty()
251 );
252 }
253
254 assert_crud_contract(&ConvexGraphStore::new(ConvexRowsGraphClient::default()));
255 }
256
257 #[test]
258 fn convex_projection_rows_keep_stable_ids_and_edge_keys() {
259 let projection = sample_projection();
260 let rows = projection.to_convex_rows();
261
262 let doc_row = rows
263 .nodes
264 .iter()
265 .find(|row| row.external_id == "doc:livekit")
266 .unwrap();
267 assert_eq!(doc_row.kind, "document");
268 assert_eq!(doc_row.properties.get("domain"), Some(&"livekit".into()));
269
270 let mentions = rows
271 .edges
272 .iter()
273 .find(|row| row.kind == "mentions")
274 .unwrap();
275 assert_eq!(mentions.from_external_id, "doc:livekit");
276 assert_eq!(mentions.to_external_id, "topic:rooms");
277 assert_eq!(
278 mentions.edge_key,
279 ConvexEdgeRow::stable_key("doc:livekit", "topic:rooms", "mentions")
280 );
281 assert!(mentions.edge_key.starts_with("edge:"));
282 }
283
284 #[test]
285 fn terse_graph_node_strips_provenance_and_freshness() {
286 let node = GraphNode::new("doc:livekit", "document", "LiveKit guide")
287 .with_property("domain", "livekit")
288 .with_provenance(GraphProvenance::new("fixture", "src/lib.rs:1"))
289 .with_freshness(GraphFreshness::content_hash("hash-1"));
290 let terse = TerseGraphNode::from(&node);
291 assert_eq!(terse.id, "doc:livekit");
292 assert_eq!(terse.kind, "document");
293 assert_eq!(terse.label, "LiveKit guide");
294 assert_eq!(terse.properties.get("domain"), Some(&"livekit".to_string()));
295 let json = serde_json::to_value(&terse).unwrap();
296 assert!(json.get("provenance").is_none());
297 assert!(json.get("freshness").is_none());
298 let full_json = serde_json::to_string(&node).unwrap();
299 let terse_json = serde_json::to_string(&terse).unwrap();
300 assert!(
301 terse_json.len() < full_json.len(),
302 "terse ({}) should be shorter than full ({})",
303 terse_json.len(),
304 full_json.len()
305 );
306 }
307
308 #[test]
309 fn terse_graph_edge_strips_provenance_and_freshness() {
310 let edge = GraphEdge::new("a", "b", "calls")
311 .with_property("line", "10")
312 .with_provenance(GraphProvenance::new("fixture", "src/lib.rs:5"))
313 .with_freshness(GraphFreshness::content_hash("edge-hash"));
314 let terse = TerseGraphEdge::from(&edge);
315 assert_eq!(terse.from_id, "a");
316 assert_eq!(terse.to_id, "b");
317 assert_eq!(terse.kind, "calls");
318 assert_eq!(terse.properties.get("line"), Some(&"10".to_string()));
319 let json = serde_json::to_value(&terse).unwrap();
320 assert!(json.get("provenance").is_none());
321 assert!(json.get("freshness").is_none());
322 }
323
324 #[test]
325 fn terse_graph_subgraph_rounds_trip() {
326 let subgraph = GraphSubgraph {
327 nodes: vec![
328 GraphNode::new("a", "fn", "alpha")
329 .with_provenance(GraphProvenance::new("src", "a.rs")),
330 GraphNode::new("b", "fn", "beta"),
331 ],
332 edges: vec![
333 GraphEdge::new("a", "b", "calls").with_freshness(GraphFreshness::content_hash("h")),
334 ],
335 }
336 .sorted();
337 let terse = TerseGraphSubgraph::from(subgraph);
338 assert_eq!(terse.nodes.len(), 2);
339 assert_eq!(terse.edges.len(), 1);
340 assert_eq!(terse.edges[0].from_id, "a");
341 }
342
343 #[test]
344 fn convex_store_rejects_edges_when_projection_nodes_are_missing() {
345 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
346 store
347 .upsert_node(&GraphNode::new("doc:livekit", "document", "LiveKit guide"))
348 .unwrap();
349
350 let err = store
351 .upsert_edge(&GraphEdge::new("doc:livekit", "topic:rooms", "mentions"))
352 .unwrap_err();
353 assert!(
354 err.to_string().contains("references missing to node"),
355 "{err}"
356 );
357 }
358
359 #[test]
360 fn ranked_neighborhood_returns_none_for_missing_center() {
361 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
362 let options = RankedNeighborhoodOptions::new(2, 10);
363 let result = store.ranked_neighborhood("missing", &options).unwrap();
364 assert!(result.is_none());
365 }
366
367 #[test]
368 fn ranked_neighborhood_breadth_first_respects_max_nodes() {
369 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
370 store
371 .upsert_node(&GraphNode::new("a", "file", "a"))
372 .unwrap();
373 store
374 .upsert_node(&GraphNode::new("b", "symbol", "b"))
375 .unwrap();
376 store
377 .upsert_node(&GraphNode::new("c", "symbol", "c"))
378 .unwrap();
379 store
380 .upsert_node(&GraphNode::new("d", "symbol", "d"))
381 .unwrap();
382 store
383 .upsert_edge(&GraphEdge::new("a", "b", "calls"))
384 .unwrap();
385 store
386 .upsert_edge(&GraphEdge::new("a", "c", "calls"))
387 .unwrap();
388 store
389 .upsert_edge(&GraphEdge::new("a", "d", "calls"))
390 .unwrap();
391
392 let options = RankedNeighborhoodOptions::new(2, 2);
393 let result = store.ranked_neighborhood("a", &options).unwrap().unwrap();
394 assert!(
395 result.nodes.len() <= 3,
396 "center + max 2 neighbors, got {}",
397 result.nodes.len()
398 );
399 assert!(result.pruned_count > 0, "should have pruned some nodes");
400 assert!(result.total_discovered >= 4);
401 }
402
403 #[test]
404 fn ranked_neighborhood_edge_kind_weighted_prefers_high_score_edges() {
405 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
406 store
407 .upsert_node(&GraphNode::new("a", "file", "a"))
408 .unwrap();
409 store
410 .upsert_node(&GraphNode::new("b", "symbol", "b"))
411 .unwrap();
412 store
413 .upsert_node(&GraphNode::new("c", "symbol", "c"))
414 .unwrap();
415 store
416 .upsert_edge(&GraphEdge::new("a", "b", "semantic_relation"))
417 .unwrap();
418 store
419 .upsert_edge(&GraphEdge::new("a", "c", "unknown"))
420 .unwrap();
421
422 let options = RankedNeighborhoodOptions::new(1, 1)
423 .with_scoring(NeighborhoodScoring::EdgeKindWeighted);
424 let result = store.ranked_neighborhood("a", &options).unwrap().unwrap();
425 let neighbor_ids: Vec<_> = result.nodes.iter().map(|n| n.id.as_str()).collect();
426 assert!(
427 neighbor_ids.contains(&"b"),
428 "semantic_relation neighbor should survive pruning"
429 );
430 }
431
432 #[test]
433 fn ranked_neighborhood_prefers_recent_memory_nodes_when_pruning() {
434 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
435 store
436 .upsert_node(&GraphNode::new("center", "file", "center"))
437 .unwrap();
438 store
439 .upsert_node(&GraphNode::new("aaa-code", "symbol", "code candidate"))
440 .unwrap();
441 store
442 .upsert_node(
443 &GraphNode::new("mmm-stale", "memory_event", "stale memory")
444 .with_property("provider", "tsift-memory")
445 .with_property("observed_at_unix", "1000"),
446 )
447 .unwrap();
448 store
449 .upsert_node(
450 &GraphNode::new("zzz-fresh", "memory_event", "fresh memory")
451 .with_property("provider", "tsift-memory")
452 .with_property("observed_at_unix", "1995"),
453 )
454 .unwrap();
455 store
456 .upsert_edge(&GraphEdge::new("center", "aaa-code", "mentions"))
457 .unwrap();
458 store
459 .upsert_edge(&GraphEdge::new("center", "mmm-stale", "mentions"))
460 .unwrap();
461 store
462 .upsert_edge(&GraphEdge::new("center", "zzz-fresh", "mentions"))
463 .unwrap();
464
465 let options = RankedNeighborhoodOptions::new(1, 1)
466 .with_observed_at_now_unix(2000)
467 .with_observed_at_half_life_secs(100);
468 let result = store
469 .ranked_neighborhood("center", &options)
470 .unwrap()
471 .unwrap();
472 let ids: Vec<_> = result.nodes.iter().map(|node| node.id.as_str()).collect();
473 assert!(ids.contains(&"center"));
474 assert!(
475 ids.contains(&"zzz-fresh"),
476 "fresh memory node should survive pruning: {ids:?}"
477 );
478 assert!(!ids.contains(&"aaa-code"));
479 assert!(!ids.contains(&"mmm-stale"));
480 }
481
482 #[test]
483 fn semantic_seeded_neighborhood_scores_before_caps() {
484 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
485 store
486 .upsert_node(&GraphNode::new("seed", "semantic_concept", "graph budget"))
487 .unwrap();
488 store
489 .upsert_node(&GraphNode::new("zzz_high", "symbol", "high_signal"))
490 .unwrap();
491 store
492 .upsert_edge(&GraphEdge::new("zzz_high", "seed", "mentions_concept"))
493 .unwrap();
494 for idx in 0..24 {
495 let id = format!("aaa_low_{idx:02}");
496 store
497 .upsert_node(&GraphNode::new(id.clone(), "note", format!("low {idx}")))
498 .unwrap();
499 store
500 .upsert_edge(&GraphEdge::new(id, "seed", "weak_link"))
501 .unwrap();
502 }
503
504 let options = SemanticSeededNeighborhoodOptions::new(1, 3)
505 .with_edge_scan_cap(16)
506 .with_node_discovery_cap(9);
507 let result = store
508 .semantic_seeded_neighborhood(&["seed".to_string()], &options)
509 .unwrap();
510 let ids = result
511 .nodes
512 .iter()
513 .map(|node| node.id.as_str())
514 .collect::<Vec<_>>();
515
516 assert_eq!(ids.len(), 3);
517 assert_eq!(ids[0], "seed");
518 assert_eq!(ids[1], "zzz_high");
519 assert!(result.skipped_by_edge_cap > 0);
520 assert!(result.truncated);
521 }
522
523 #[test]
524 fn ranked_neighborhood_includes_center_node() {
525 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
526 store
527 .upsert_node(&GraphNode::new("center", "file", "center"))
528 .unwrap();
529 store
530 .upsert_node(&GraphNode::new("neighbor", "symbol", "neighbor"))
531 .unwrap();
532 store
533 .upsert_edge(&GraphEdge::new("center", "neighbor", "calls"))
534 .unwrap();
535
536 let options = RankedNeighborhoodOptions::new(1, 10);
537 let result = store
538 .ranked_neighborhood("center", &options)
539 .unwrap()
540 .unwrap();
541 let ids: Vec<_> = result.nodes.iter().map(|n| n.id.clone()).collect();
542 assert!(ids.contains(&"center".to_string()));
543 assert!(ids.contains(&"neighbor".to_string()));
544 assert_eq!(result.pruned_count, 0);
545 }
546
547 #[test]
548 fn ranked_neighborhood_edge_kind_filter() {
549 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
550 store
551 .upsert_node(&GraphNode::new("a", "file", "a"))
552 .unwrap();
553 store
554 .upsert_node(&GraphNode::new("b", "symbol", "b"))
555 .unwrap();
556 store
557 .upsert_node(&GraphNode::new("c", "symbol", "c"))
558 .unwrap();
559 store
560 .upsert_edge(&GraphEdge::new("a", "b", "calls"))
561 .unwrap();
562 store
563 .upsert_edge(&GraphEdge::new("a", "c", "mentions"))
564 .unwrap();
565
566 let options = RankedNeighborhoodOptions::new(1, 10).with_edge_kind("calls");
567 let result = store.ranked_neighborhood("a", &options).unwrap().unwrap();
568 let ids: Vec<_> = result.nodes.iter().map(|n| n.id.as_str()).collect();
569 assert!(ids.contains(&"b"));
570 assert!(!ids.contains(&"c"), "mentions edge should be filtered out");
571 }
572
573 #[test]
574 fn ranked_neighborhood_degree_weighted_prefers_low_degree() {
575 let store = ConvexGraphStore::new(MemoryConvexGraphClient::default());
576 store
577 .upsert_node(&GraphNode::new("a", "file", "a"))
578 .unwrap();
579 store
580 .upsert_node(&GraphNode::new("b", "symbol", "b"))
581 .unwrap();
582 store
583 .upsert_node(&GraphNode::new("c", "symbol", "c"))
584 .unwrap();
585 store
586 .upsert_node(&GraphNode::new("d", "symbol", "d"))
587 .unwrap();
588 store
589 .upsert_node(&GraphNode::new("e", "symbol", "e"))
590 .unwrap();
591 store
592 .upsert_edge(&GraphEdge::new("a", "b", "calls"))
593 .unwrap();
594 store
595 .upsert_edge(&GraphEdge::new("b", "c", "calls"))
596 .unwrap();
597 store
598 .upsert_edge(&GraphEdge::new("b", "d", "calls"))
599 .unwrap();
600 store
601 .upsert_edge(&GraphEdge::new("b", "e", "calls"))
602 .unwrap();
603
604 let options =
605 RankedNeighborhoodOptions::new(2, 3).with_scoring(NeighborhoodScoring::DegreeWeighted);
606 let result = store.ranked_neighborhood("a", &options).unwrap().unwrap();
607 assert!(result.nodes.len() <= 4);
608 }
609
610 #[test]
611 fn compute_neighborhood_score_breadth_first() {
612 let score = store::compute_neighborhood_score(
613 &NeighborhoodScoring::BreadthFirst,
614 0,
615 "calls",
616 &GraphNode::new("x", "symbol", "x"),
617 &BTreeMap::new(),
618 );
619 assert_eq!(score, 120);
620 let score_d2 = store::compute_neighborhood_score(
621 &NeighborhoodScoring::BreadthFirst,
622 2,
623 "calls",
624 &GraphNode::new("x", "symbol", "x"),
625 &BTreeMap::new(),
626 );
627 assert_eq!(score_d2, 84);
628 }
629
630 #[test]
631 fn compute_neighborhood_score_edge_kind_weighted() {
632 let score_semantic = store::compute_neighborhood_score(
633 &NeighborhoodScoring::EdgeKindWeighted,
634 1,
635 "semantic_relation",
636 &GraphNode::new("x", "symbol", "x"),
637 &BTreeMap::new(),
638 );
639 let score_unknown = store::compute_neighborhood_score(
640 &NeighborhoodScoring::EdgeKindWeighted,
641 1,
642 "unknown",
643 &GraphNode::new("x", "symbol", "x"),
644 &BTreeMap::new(),
645 );
646 assert!(score_semantic > score_unknown);
647 }
648
649 #[test]
650 fn compute_ranked_neighborhood_score_applies_decay_and_memory_signal() {
651 let options = RankedNeighborhoodOptions::new(1, 1)
652 .with_observed_at_now_unix(2_000)
653 .with_observed_at_half_life_secs(100)
654 .with_observed_at_weight(24)
655 .with_memory_node_boost(18);
656 let context = store::NeighborhoodScoreContext::from_options(&options);
657 let code = GraphNode::new("code", "symbol", "code");
658 let stale_memory = GraphNode::new("stale", "memory_event", "stale")
659 .with_property("provider", "tsift-memory")
660 .with_property("observed_at_unix", "1000");
661 let fresh_memory = GraphNode::new("fresh", "memory_event", "fresh")
662 .with_property("provider", "tsift-memory")
663 .with_property("observed_at_unix", "1995");
664
665 let code_score = store::compute_ranked_neighborhood_score(
666 &options,
667 context,
668 1,
669 "mentions",
670 &code,
671 &BTreeMap::new(),
672 );
673 let stale_score = store::compute_ranked_neighborhood_score(
674 &options,
675 context,
676 1,
677 "mentions",
678 &stale_memory,
679 &BTreeMap::new(),
680 );
681 let fresh_score = store::compute_ranked_neighborhood_score(
682 &options,
683 context,
684 1,
685 "mentions",
686 &fresh_memory,
687 &BTreeMap::new(),
688 );
689
690 assert!(fresh_score > stale_score);
691 assert!(stale_score > code_score);
692 }
693}