use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::property::PropertyValue;
use crate::core::temporal::Timestamp;
use std::time::Duration;
#[derive(Debug, Clone)]
pub enum Clue {
PropertyState {
key: String,
value: Option<PropertyValue>,
},
}
#[cfg(feature = "semantic-temporal")]
#[derive(Debug, Clone)]
pub struct Mystery {
pub clues: Vec<Clue>,
pub time_window: Duration,
}
#[cfg(not(feature = "semantic-temporal"))]
#[deprecated(
note = "Mystery requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
pub struct Mystery {
_marker: std::marker::PhantomData<Duration>,
}
#[cfg(feature = "semantic-temporal")]
impl Mystery {
pub fn new(time_window: Duration) -> Self {
Self {
clues: Vec::new(),
time_window,
}
}
pub fn add_clue(mut self, clue: Clue) -> Self {
self.clues.push(clue);
self
}
}
#[cfg(not(feature = "semantic-temporal"))]
#[allow(deprecated)]
impl Mystery {
#[allow(unused_variables)]
#[track_caller]
pub fn new(time_window: Duration) -> Self {
panic!(
"Mystery requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
#[allow(unused_variables)]
#[track_caller]
pub fn add_clue(self, clue: Clue) -> Self {
panic!(
"Mystery requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
}
#[derive(Debug, Clone)]
pub struct Deduction {
pub node_id: NodeId,
pub event_times: Vec<Timestamp>,
}
#[cfg(feature = "semantic-temporal")]
pub struct Sherlock<'a> {
#[allow(dead_code)]
db: &'a AletheiaDB,
}
#[cfg(not(feature = "semantic-temporal"))]
#[deprecated(
note = "Sherlock requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
pub struct Sherlock<'a> {
_marker: std::marker::PhantomData<&'a AletheiaDB>,
}
#[cfg(feature = "semantic-temporal")]
impl<'a> Sherlock<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn investigate(&self, node_id: NodeId, mystery: &Mystery) -> Result<Vec<Deduction>> {
let history = self.db.get_node_history(node_id)?;
let mut deductions = Vec::new();
if mystery.clues.is_empty() {
return Ok(deductions);
}
let mut versions = history.versions.clone();
versions.sort_by_key(|v| v.temporal.valid_time().start());
for (i, version) in versions.iter().enumerate() {
if self.matches_clue(version, &mystery.clues[0]) {
let mut current_sequence = vec![version.temporal.valid_time().start()];
let start_time = current_sequence[0];
let mut current_version_idx = i;
let mut matched_so_far = true;
for next_clue_idx in 1..mystery.clues.len() {
let mut found_next = false;
let prev_event_time = *current_sequence.last().unwrap();
for (j, candidate) in versions
.iter()
.enumerate()
.skip(current_version_idx.saturating_add(1))
{
let candidate_time = candidate.temporal.valid_time().start();
if candidate_time <= prev_event_time {
continue;
}
let elapsed_micros = candidate_time.wallclock() - start_time.wallclock();
if elapsed_micros > mystery.time_window.as_micros() as i64 {
break;
}
if self.matches_clue(candidate, &mystery.clues[next_clue_idx]) {
current_sequence.push(candidate_time);
current_version_idx = j;
found_next = true;
break; }
}
if !found_next {
matched_so_far = false;
break;
}
}
if matched_so_far {
deductions.push(Deduction {
node_id,
event_times: current_sequence,
});
}
}
}
Ok(deductions)
}
fn matches_clue(&self, version: &crate::core::history::VersionInfo, clue: &Clue) -> bool {
match clue {
Clue::PropertyState { key, value } => {
if let Some(prop_val) = version.properties.get(key) {
if let Some(target_val) = value {
prop_val == target_val
} else {
true }
} else {
false }
}
}
}
}
#[cfg(not(feature = "semantic-temporal"))]
#[allow(deprecated)]
impl<'a> Sherlock<'a> {
#[allow(unused_variables)]
#[track_caller]
pub fn new(db: &'a AletheiaDB) -> Self {
panic!(
"Sherlock requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
#[allow(unused_variables)]
#[track_caller]
pub fn investigate(&self, node_id: NodeId, mystery: &Mystery) -> Result<Vec<Deduction>> {
panic!(
"Sherlock requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
}
#[cfg(all(test, feature = "semantic-temporal"))]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
use crate::core::temporal::time;
#[test]
fn test_sherlock_detects_sequence() {
let db = AletheiaDB::new().unwrap();
let t0 = time::from_millis(1_000);
let t1 = time::from_millis(1_050);
let t2 = time::from_millis(1_100);
let props = PropertyMapBuilder::new()
.insert("status", "Pending")
.build();
let node_id = db
.write(|tx| tx.create_node_with_valid_time("Order", props, Some(t0)))
.unwrap();
db.write(|tx| {
let p = PropertyMapBuilder::new()
.insert("status", "Shipped")
.build();
tx.update_node_with_valid_time(node_id, p, Some(t1))
})
.unwrap();
db.write(|tx| {
let p = PropertyMapBuilder::new()
.insert("status", "Delivered")
.build();
tx.update_node_with_valid_time(node_id, p, Some(t2))
})
.unwrap();
let sherlock = Sherlock::new(&db);
let mystery = Mystery::new(Duration::from_secs(1))
.add_clue(Clue::PropertyState {
key: "status".to_string(),
value: Some(PropertyValue::from("Pending")),
})
.add_clue(Clue::PropertyState {
key: "status".to_string(),
value: Some(PropertyValue::from("Shipped")),
})
.add_clue(Clue::PropertyState {
key: "status".to_string(),
value: Some(PropertyValue::from("Delivered")),
});
let results = sherlock.investigate(node_id, &mystery).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].node_id, node_id);
assert_eq!(results[0].event_times, vec![t0, t1, t2]);
}
#[test]
fn test_sherlock_time_window_constraint() {
let db = AletheiaDB::new().unwrap();
let t0 = time::from_millis(10_000);
let t1 = time::from_millis(10_100);
let props = PropertyMapBuilder::new().insert("state", "A").build();
let node_id = db
.write(|tx| tx.create_node_with_valid_time("Machine", props, Some(t0)))
.unwrap();
db.write(|tx| {
let p = PropertyMapBuilder::new().insert("state", "B").build();
tx.update_node_with_valid_time(node_id, p, Some(t1))
})
.unwrap();
let sherlock = Sherlock::new(&db);
let impossible_mystery = Mystery::new(Duration::from_millis(10))
.add_clue(Clue::PropertyState {
key: "state".to_string(),
value: Some(PropertyValue::from("A")),
})
.add_clue(Clue::PropertyState {
key: "state".to_string(),
value: Some(PropertyValue::from("B")),
});
let results = sherlock.investigate(node_id, &impossible_mystery).unwrap();
assert!(
results.is_empty(),
"Should not match due to time constraint"
);
let possible_mystery = Mystery::new(Duration::from_millis(500))
.add_clue(Clue::PropertyState {
key: "state".to_string(),
value: Some(PropertyValue::from("A")),
})
.add_clue(Clue::PropertyState {
key: "state".to_string(),
value: Some(PropertyValue::from("B")),
});
let results_pass = sherlock.investigate(node_id, &possible_mystery).unwrap();
assert_eq!(results_pass.len(), 1, "Should match within 500ms");
assert_eq!(results_pass[0].node_id, node_id);
assert_eq!(results_pass[0].event_times, vec![t0, t1]);
}
}
#[cfg(all(test, not(feature = "semantic-temporal")))]
#[allow(deprecated)]
mod stub_tests {
use super::*;
#[test]
#[should_panic(
expected = "Sherlock requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_new() {
let db = AletheiaDB::new().unwrap();
let _ = Sherlock::new(&db);
}
#[test]
#[should_panic(
expected = "Mystery requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_mystery() {
let _ = Mystery::new(Duration::from_secs(1));
}
#[test]
#[should_panic(
expected = "Mystery requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_add_clue() {
let mystery = Mystery {
_marker: std::marker::PhantomData,
};
let clue = Clue::PropertyState {
key: "test".to_string(),
value: None,
};
let _ = mystery.add_clue(clue);
}
#[test]
#[should_panic(
expected = "Sherlock requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_investigate() {
let sherlock = Sherlock {
_marker: std::marker::PhantomData,
};
let mystery = Mystery {
_marker: std::marker::PhantomData,
};
let _ = sherlock.investigate(NodeId::new(0).unwrap(), &mystery);
}
}