use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq)]
pub struct Wormhole {
pub source: NodeId,
pub target: NodeId,
pub similarity: f32,
pub structural_distance: Option<usize>,
}
pub struct WormholeDetector<'a> {
db: &'a AletheiaDB,
}
impl<'a> WormholeDetector<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn find_wormholes(
&self,
candidates: &[NodeId],
k: usize,
max_hops: usize,
) -> Result<Vec<Wormhole>> {
let mut wormholes = Vec::new();
for &source in candidates {
let semantic_neighbors = self.db.find_similar(source, k)?;
for (target, similarity) in semantic_neighbors {
let distance = self.bfs_distance(source, target, max_hops)?;
if distance.is_none() {
wormholes.push(Wormhole {
source,
target,
similarity,
structural_distance: None,
});
}
}
}
Ok(wormholes)
}
fn bfs_distance(&self, start: NodeId, end: NodeId, max_depth: usize) -> Result<Option<usize>> {
if start == end {
return Ok(Some(0));
}
let mut queue = VecDeque::new();
queue.push_back((start, 0));
let mut visited = HashSet::new();
visited.insert(start);
while let Some((current, depth)) = queue.pop_front() {
if depth >= max_depth {
continue;
}
let edges_iter = self.db.current.get_outgoing_edges_iter(current);
for edge_id in edges_iter {
let target = self.db.current.get_edge_target(edge_id)?;
if target == end {
return Ok(Some(depth + 1));
}
if visited.insert(target) {
queue.push_back((target, depth + 1));
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_wormhole_detection() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_b = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let b = db.create_node("Node", props_b).unwrap();
let props_c = PropertyMapBuilder::new()
.insert_vector("vec", &[0.9, 0.1])
.build();
let c = db.create_node("Node", props_c).unwrap();
db.create_edge(a, b, "NEXT", PropertyMapBuilder::new().build())
.unwrap();
db.create_edge(b, c, "NEXT", PropertyMapBuilder::new().build())
.unwrap();
let detector = WormholeDetector::new(&db);
let wormholes = detector.find_wormholes(&[a], 10, 1).unwrap();
let ac = wormholes.iter().find(|w| w.target == c);
assert!(ac.is_some(), "Should find wormhole A -> C with max_hops=1");
let w = ac.unwrap();
assert_eq!(w.source, a);
assert_eq!(w.target, c);
assert!(w.structural_distance.is_none());
assert!(w.similarity > 0.8);
let wormholes_2 = detector.find_wormholes(&[a], 10, 2).unwrap();
let ac_2 = wormholes_2.iter().find(|w| w.target == c);
assert!(
ac_2.is_none(),
"Should NOT find wormhole A -> C with max_hops=2 (path exists)"
);
}
#[test]
fn test_wormhole_disconnected() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Cosine);
db.enable_vector_index("vec", config).unwrap();
let props_a = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let a = db.create_node("Node", props_a).unwrap();
let props_d = PropertyMapBuilder::new()
.insert_vector("vec", &[0.95, 0.0])
.build();
let d = db.create_node("Node", props_d).unwrap();
let detector = WormholeDetector::new(&db);
let wormholes = detector.find_wormholes(&[a], 10, 5).unwrap();
assert!(!wormholes.is_empty());
assert_eq!(wormholes[0].target, d);
assert!(wormholes[0].structural_distance.is_none());
}
}