use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::temporal::{Timestamp, time};
use crate::core::vector::cosine_similarity;
#[derive(Debug, Clone)]
pub struct TimelineEvent {
pub timestamp: String,
pub timestamp_obj: Timestamp,
pub version_id: u64,
pub event_type: EventType,
pub description: String,
pub score: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EventType {
Created,
SemanticJump {
distance: f32,
},
PropertyChange {
key: String,
},
}
pub struct Kairos<'a> {
db: &'a AletheiaDB,
}
impl<'a> Kairos<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn extract_timeline(
&self,
node_id: NodeId,
vector_property: &str,
significance_threshold: f32,
) -> Result<Vec<TimelineEvent>> {
let history = self.db.get_node_history(node_id)?;
let mut timeline = Vec::new();
let mut last_significant_vector: Option<Vec<f32>> = None;
for (i, version) in history.versions.iter().enumerate() {
let timestamp_obj = version.temporal.transaction_time().start();
let timestamp = time::to_iso8601(timestamp_obj);
let version_id = version.version_number;
if i == 0 {
if let Some(vec) = version
.properties
.get(vector_property)
.and_then(|v| v.as_vector())
{
last_significant_vector = Some(vec.to_vec());
}
timeline.push(TimelineEvent {
timestamp,
timestamp_obj,
version_id,
event_type: EventType::Created,
description: format!("Node created with label '{}'", version.label),
score: 1.0, });
continue;
}
let current_vector = version
.properties
.get(vector_property)
.and_then(|v| v.as_vector());
if let Some(current) = current_vector {
if let Some(baseline) = &last_significant_vector {
let similarity = cosine_similarity(baseline, current)?;
let dist = 1.0 - similarity;
if dist >= significance_threshold {
timeline.push(TimelineEvent {
timestamp: timestamp.clone(),
timestamp_obj,
version_id,
event_type: EventType::SemanticJump { distance: dist },
description: format!(
"Significant semantic shift (distance: {:.4})",
dist
),
score: dist,
});
last_significant_vector = Some(current.to_vec());
}
} else {
timeline.push(TimelineEvent {
timestamp: timestamp.clone(),
timestamp_obj,
version_id,
event_type: EventType::SemanticJump { distance: 1.0 },
description: "Vector property initialized/added".to_string(),
score: 1.0,
});
last_significant_vector = Some(current.to_vec());
}
} else {
if last_significant_vector.is_some() {
last_significant_vector = None;
}
}
if i > 0 {
let prev = &history.versions[i - 1];
let diff = crate::core::history::VersionDiff::compute(
&prev.properties,
&version.properties,
prev.version_id,
version.version_id,
);
let mut significant_props = Vec::new();
for (key, _) in diff.added.iter() {
let key_str = crate::core::GLOBAL_INTERNER
.resolve_with(*key, |s| s.to_string())
.unwrap();
if key_str != vector_property {
significant_props.push(format!("Added '{}'", key_str));
}
}
for (key, _, _) in diff.modified.iter() {
let key_str = crate::core::GLOBAL_INTERNER
.resolve_with(*key, |s| s.to_string())
.unwrap();
if key_str != vector_property {
significant_props.push(format!("Modified '{}'", key_str));
}
}
for (key, _) in diff.removed.iter() {
let key_str = crate::core::GLOBAL_INTERNER
.resolve_with(*key, |s| s.to_string())
.unwrap();
if key_str != vector_property {
significant_props.push(format!("Removed '{}'", key_str));
}
}
if !significant_props.is_empty() {
timeline.push(TimelineEvent {
timestamp,
timestamp_obj,
version_id,
event_type: EventType::PropertyChange {
key: "multiple".to_string(),
},
description: format!(
"Properties changed: {}",
significant_props.join(", ")
),
score: 0.5, });
}
}
}
Ok(timeline)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_kairos_noise_filtering() {
let db = AletheiaDB::new().unwrap();
db.enable_vector_index("vec", HnswConfig::new(2, DistanceMetric::Cosine))
.unwrap();
let props = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let node = db.create_node("Test", props).unwrap();
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new()
.insert_vector("vec", &[0.999, 0.045])
.build(),
)
})
.unwrap();
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new()
.insert_vector("vec", &[0.995, 0.1])
.build(),
)
})
.unwrap();
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build(),
)
})
.unwrap();
let kairos = Kairos::new(&db);
let timeline = kairos.extract_timeline(node, "vec", 0.1).unwrap();
assert_eq!(timeline.len(), 2);
assert_eq!(timeline[0].event_type, EventType::Created);
match &timeline[1].event_type {
EventType::SemanticJump { distance } => {
assert!(*distance > 0.1, "Distance should be significant");
}
_ => panic!("Expected SemanticJump"),
}
}
#[test]
fn test_kairos_property_changes() {
let db = AletheiaDB::new().unwrap();
let props = PropertyMapBuilder::new().insert("name", "Alice").build();
let node = db.create_node("Person", props).unwrap();
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new().insert("name", "Bob").build(),
)
})
.unwrap();
let kairos = Kairos::new(&db);
let timeline = kairos.extract_timeline(node, "vec", 1.0).unwrap();
assert_eq!(timeline.len(), 2);
assert_eq!(
timeline[1].description,
"Properties changed: Modified 'name'"
);
}
}