#![allow(clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::temporal::Timestamp;
use crate::core::vector::ops;
pub struct ParadoxDetector<'a> {
db: &'a AletheiaDB,
}
impl<'a> ParadoxDetector<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn calculate_paradox(
&self,
subject_node: NodeId,
target_vector: &[f32],
property_name: &str,
t1: Timestamp,
t2: Timestamp,
) -> Result<f32> {
let sem_t1 =
self.get_semantic_similarity(subject_node, target_vector, property_name, t1)?;
let sem_t2 =
self.get_semantic_similarity(subject_node, target_vector, property_name, t2)?;
let semantic_delta = sem_t2 - sem_t1;
let struct_t1 =
self.get_structural_affinity(subject_node, target_vector, property_name, t1)?;
let struct_t2 =
self.get_structural_affinity(subject_node, target_vector, property_name, t2)?;
let structural_delta = struct_t2 - struct_t1;
let raw_paradox = -(semantic_delta * structural_delta);
let paradox_score = (raw_paradox * 4.0).clamp(-1.0, 1.0);
Ok(paradox_score)
}
fn get_semantic_similarity(
&self,
node_id: NodeId,
target_vector: &[f32],
property_name: &str,
time: Timestamp,
) -> Result<f32> {
let mut vector_opt = None;
if let Ok(history) = self.db.get_node_history(node_id) {
let mut latest_valid_version = None;
for version in history.versions.iter() {
if version.temporal.valid_time().start() <= time && time <= version.temporal.valid_time().end() {
if version.temporal.tx_time().start() <= time {
latest_valid_version = Some(version);
}
}
}
if let Some(v) = latest_valid_version {
vector_opt = v.properties.get(property_name).and_then(|p| p.as_vector()).map(|v| v.to_vec());
}
}
if vector_opt.is_none() {
let _ = self.db.read(|tx| {
if let Ok(node) = tx.get_node(node_id) {
vector_opt = node.get_property(property_name).and_then(|p| p.as_vector()).map(|v| v.to_vec());
}
Ok::<(), Error>(())
});
}
if let Some(mut v1) = vector_opt {
let mut v2 = target_vector.to_vec();
ops::normalize_in_place(&mut v1);
ops::normalize_in_place(&mut v2);
ops::cosine_similarity(&v1, &v2)
} else {
Ok(0.0)
}
}
fn get_structural_affinity(
&self,
node_id: NodeId,
target_vector: &[f32],
property_name: &str,
time: Timestamp,
) -> Result<f32> {
let mut edges = Vec::new();
if let Ok(results) = self.db.query().as_of(time, time).start(node_id).traverse_all().execute(self.db) {
for row in results {
if let Ok(r) = row {
if let Some(neighbor) = r.entity.as_node() {
if neighbor.id != node_id {
edges.push(neighbor.id);
}
}
}
}
}
if edges.is_empty() {
let _ = self.db.read(|tx| {
edges = tx.get_outgoing_edges(node_id).into_iter().filter_map(|eid| {
if let Ok(edge) = tx.get_edge(eid) {
Some(edge.target)
} else {
None
}
}).collect();
Ok::<(), Error>(())
});
}
let mut total_sim = 0.0;
let mut count = 0;
for neighbor_id in edges {
let sim = self.get_semantic_similarity(neighbor_id, target_vector, property_name, time).unwrap_or(0.0);
if sim != 0.0 {
total_sim += sim;
count += 1;
}
}
if count == 0 {
Ok(0.0)
} else {
Ok(total_sim / count as f32)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AletheiaDB;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_paradox_detection() {
let sem_t1 = 0.1; let sem_t2 = 0.9; let semantic_delta = sem_t2 - sem_t1;
let struct_t1 = 0.8; let struct_t2 = 0.2; let structural_delta = struct_t2 - struct_t1;
let raw_paradox = -(semantic_delta * structural_delta); let paradox_score = (raw_paradox * 4.0).clamp(-1.0, 1.0);
assert!(paradox_score > 0.0, "Expected a positive paradox score, got {}", paradox_score);
}
}