use crate::AletheiaDB;
#[cfg(feature = "semantic-temporal")]
use crate::core::GLOBAL_INTERNER;
use crate::core::error::Result;
#[cfg(feature = "semantic-temporal")]
use crate::core::history::{VersionDiff, VersionInfo};
use crate::core::id::NodeId;
#[cfg(feature = "semantic-temporal")]
use crate::core::interning::InternedString;
#[cfg(feature = "semantic-temporal")]
use crate::core::temporal::time;
#[derive(Debug, Clone)]
pub struct NarrativeEvent {
pub timestamp: String,
pub version_number: u64,
pub description: String,
pub changes: Vec<String>,
}
#[cfg(feature = "semantic-temporal")]
pub struct NarrativeGenerator<'a> {
db: &'a AletheiaDB,
}
#[cfg(not(feature = "semantic-temporal"))]
#[deprecated(
note = "NarrativeGenerator requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
pub struct NarrativeGenerator<'a> {
_marker: std::marker::PhantomData<&'a AletheiaDB>,
}
#[cfg(feature = "semantic-temporal")]
impl<'a> NarrativeGenerator<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
fn resolve_key(key_id: InternedString) -> String {
GLOBAL_INTERNER
.resolve_with(key_id, |s| s.to_string())
.unwrap_or_else(|| "unknown".to_string())
}
pub fn generate_node_narrative(&self, node_id: NodeId) -> Result<Vec<NarrativeEvent>> {
let history = self.db.get_node_history(node_id)?;
let mut events = Vec::new();
let mut prev_version: Option<&VersionInfo> = None;
for version in &history.versions {
let timestamp = time::to_iso8601(version.temporal.transaction_time().start());
let version_number = version.version_number;
let mut changes = Vec::new();
let description;
if let Some(prev) = prev_version {
let diff = VersionDiff::compute(
&prev.properties,
&version.properties,
prev.version_id,
version.version_id,
);
description = format!("Version {} updated properties.", version_number);
changes.reserve(diff.added.len() + diff.removed.len() + diff.modified.len());
for (key_id, val) in diff.added.iter() {
let key = Self::resolve_key(*key_id);
changes.push(format!("Added property '{}' with value '{}'", key, val));
}
for (key_id, val) in diff.removed.iter() {
let key = Self::resolve_key(*key_id);
changes.push(format!("Removed property '{}' (was '{}')", key, val));
}
for (key_id, old_val, new_val) in &diff.modified {
let key = Self::resolve_key(*key_id);
if new_val.is_null() {
changes.push(format!("Removed property '{}' (was '{}')", key, old_val));
} else {
changes.push(format!(
"Modified property '{}' from '{}' to '{}'",
key, old_val, new_val
));
}
}
} else {
description = format!("Node created with label '{}'.", version.label);
changes.reserve(version.properties.len());
for (key_id, val) in version.properties.iter() {
let key = Self::resolve_key(*key_id);
changes.push(format!("Initial property '{}': '{}'", key, val));
}
}
events.push(NarrativeEvent {
timestamp,
version_number,
description,
changes,
});
prev_version = Some(version);
}
Ok(events)
}
}
#[cfg(not(feature = "semantic-temporal"))]
#[allow(deprecated)]
impl<'a> NarrativeGenerator<'a> {
#[allow(unused_variables)]
#[track_caller]
pub fn new(db: &'a AletheiaDB) -> Self {
panic!(
"NarrativeGenerator requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
#[allow(unused_variables)]
#[track_caller]
pub fn generate_node_narrative(&self, node_id: NodeId) -> Result<Vec<NarrativeEvent>> {
panic!(
"NarrativeGenerator 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, PropertyValue};
#[test]
fn test_node_narrative_generation() {
let db = AletheiaDB::new().unwrap();
let props1 = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.build();
let node_id = db.create_node("Person", props1).unwrap();
db.write(|tx| {
let props2 = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 31i64)
.insert("city", "London")
.build();
tx.update_node(node_id, props2)
})
.unwrap();
let generator = NarrativeGenerator::new(&db);
let narrative = generator.generate_node_narrative(node_id).unwrap();
assert_eq!(narrative.len(), 2);
let event1 = &narrative[0];
assert_eq!(event1.version_number, 1);
assert!(event1.description.contains("Node created"));
assert!(
event1
.changes
.iter()
.any(|s| s.contains("Initial property 'name': '\"Alice\"'"))
);
assert!(
event1
.changes
.iter()
.any(|s| s.contains("Initial property 'age': '30'"))
);
let event2 = &narrative[1];
assert_eq!(event2.version_number, 2);
assert!(event2.description.contains("updated properties"));
assert!(
event2
.changes
.iter()
.any(|s| s.contains("Modified property 'age' from '30' to '31'"))
);
assert!(
event2
.changes
.iter()
.any(|s| s.contains("Added property 'city' with value '\"London\"'"))
);
}
#[test]
fn test_property_removal_narrative() {
let db = AletheiaDB::new().unwrap();
let props1 = PropertyMapBuilder::new()
.insert("name", "Bob")
.insert("temp_data", "delete_me")
.build();
let node_id = db.create_node("Person", props1).unwrap();
db.write(|tx| {
let props2 = PropertyMapBuilder::new()
.insert("temp_data", PropertyValue::Null)
.build();
tx.update_node(node_id, props2)
})
.unwrap();
let generator = NarrativeGenerator::new(&db);
let narrative = generator.generate_node_narrative(node_id).unwrap();
assert_eq!(narrative.len(), 2);
let event2 = &narrative[1];
assert!(
event2
.changes
.iter()
.any(|s| s.contains("Removed property 'temp_data'")),
"Expected removal message for temp_data, found: {:?}",
event2.changes
);
assert!(
event2
.changes
.iter()
.any(|s| s.contains("was '\"delete_me\"'")),
"Expected original value in removal message"
);
}
}
#[cfg(all(test, not(feature = "semantic-temporal")))]
#[allow(deprecated)]
mod stub_tests {
use super::*;
#[test]
#[should_panic(
expected = "NarrativeGenerator requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_new() {
let db = AletheiaDB::new().unwrap();
let _ = NarrativeGenerator::new(&db);
}
#[test]
#[should_panic(
expected = "NarrativeGenerator requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_generate() {
let generator: NarrativeGenerator<'_> = NarrativeGenerator {
_marker: std::marker::PhantomData,
};
let _ = generator.generate_node_narrative(NodeId::new(0).unwrap());
}
}