//! Storage for gold standard assertions. //! //! Gold standards are stored at `{subject}\x00GS:{predicate}` with a secondary //! index at `\x00GS_LIST:{subject}:{predicate}` for listing all gold standards. use crate::{key_codec, KVStore, Result, StorageError}; use async_trait::async_trait; use std::sync::Arc; use stemedb_core::types::GoldStandard; use tracing::{debug, instrument}; /// Storage trait for gold standard operations. /// /// Provides operations for creating, reading, listing, and removing gold standards /// that define ground truth for agent verification. #[async_trait] pub trait GoldStandardStore: Send + Sync { /// Store a gold standard. /// /// Key format: `GS:{subject}:{predicate}` /// /// # Arguments /// /// - `gs` - The gold standard to store async fn set_gold_standard(&self, gs: &GoldStandard) -> Result<()>; /// Get a gold standard by subject and predicate. /// /// # Arguments /// /// - `subject` - Subject entity (e.g., "Earth") /// - `predicate` - Predicate (e.g., "has_shape") /// /// # Returns /// /// `Some(GoldStandard)` if found, `None` otherwise. async fn get_gold_standard( &self, subject: &str, predicate: &str, ) -> Result>; /// List all gold standards. /// /// Returns all gold standards ordered by subject and predicate. async fn list_gold_standards(&self) -> Result>; /// Remove a gold standard. /// /// # Arguments /// /// - `subject` - Subject entity /// - `predicate` - Predicate /// /// # Returns /// /// `true` if the gold standard was found and removed, `false` if not found. async fn remove_gold_standard(&self, subject: &str, predicate: &str) -> Result; } /// Generic implementation of `GoldStandardStore` backed by any `KVStore`. pub struct GenericGoldStandardStore { store: Arc, } impl GenericGoldStandardStore { /// Create a new gold standard store backed by the given KV store. pub fn new(store: Arc) -> Self { Self { store } } } #[async_trait] impl GoldStandardStore for GenericGoldStandardStore { #[instrument(skip(self, gs), fields(subject = %gs.subject, predicate = %gs.predicate))] async fn set_gold_standard(&self, gs: &GoldStandard) -> Result<()> { let key = key_codec::gold_standard_key(&gs.subject, &gs.predicate); let list_key = key_codec::gs_list_key(&gs.subject, &gs.predicate); let serialized = stemedb_core::serde::serialize(gs) .map_err(|e| StorageError::Serialization(e.to_string()))?; // Write primary key self.store.put(&key, &serialized).await?; // Write secondary index for listing (empty value, just presence matters) self.store.put(&list_key, &[]).await?; debug!( subject = %gs.subject, predicate = %gs.predicate, assertion_hash = %hex::encode(gs.assertion_hash), "Stored gold standard" ); Ok(()) } #[instrument(skip(self), fields(subject = %subject, predicate = %predicate))] async fn get_gold_standard( &self, subject: &str, predicate: &str, ) -> Result> { let key = key_codec::gold_standard_key(subject, predicate); match self.store.get(&key).await? { Some(data) => { let gs: GoldStandard = stemedb_core::serde::deserialize(&data) .map_err(|e| StorageError::Serialization(e.to_string()))?; debug!( subject = %subject, predicate = %predicate, expected_object = %gs.expected_object, "Retrieved gold standard" ); Ok(Some(gs)) } None => { debug!( subject = %subject, predicate = %predicate, "Gold standard not found" ); Ok(None) } } } #[instrument(skip(self))] async fn list_gold_standards(&self) -> Result> { // Scan the GS_LIST secondary index let list_entries = self.store.scan_prefix(&key_codec::gs_list_scan_prefix()).await?; let mut gold_standards = Vec::new(); for (list_key, _) in list_entries { // Extract subject and predicate from GS_LIST key: \x00GS_LIST:{subject}:{predicate} let tag = key_codec::extract_tag(&list_key); if let Some(suffix) = tag.strip_prefix(b"GS_LIST:") { if let Ok(suffix_str) = std::str::from_utf8(suffix) { // Split by first colon to get subject and predicate if let Some(colon_pos) = suffix_str.find(':') { let subject = &suffix_str[..colon_pos]; let predicate = &suffix_str[colon_pos + 1..]; // Fetch the actual gold standard from the primary key let key = key_codec::gold_standard_key(subject, predicate); if let Some(data) = self.store.get(&key).await? { match stemedb_core::serde::deserialize::(&data) { Ok(gs) => gold_standards.push(gs), Err(e) => { debug!(error = %e, subject = %subject, predicate = %predicate, "Skipping malformed gold standard"); } } } } } } } // Sort by subject, then predicate for deterministic output gold_standards .sort_by(|a, b| a.subject.cmp(&b.subject).then_with(|| a.predicate.cmp(&b.predicate))); debug!(count = gold_standards.len(), "Listed gold standards"); Ok(gold_standards) } #[instrument(skip(self), fields(subject = %subject, predicate = %predicate))] async fn remove_gold_standard(&self, subject: &str, predicate: &str) -> Result { let key = key_codec::gold_standard_key(subject, predicate); let list_key = key_codec::gs_list_key(subject, predicate); // Check if it exists first let exists = self.store.get(&key).await?.is_some(); if exists { // Delete both primary key and secondary index self.store.delete(&key).await?; self.store.delete(&list_key).await?; debug!( subject = %subject, predicate = %predicate, "Removed gold standard" ); Ok(true) } else { debug!( subject = %subject, predicate = %predicate, "Gold standard not found for removal" ); Ok(false) } } } #[cfg(test)] mod tests { use super::*; use crate::HybridStore; use stemedb_core::types::GoldStandard; fn create_gold_standard(subject: &str, predicate: &str, expected_object: &str) -> GoldStandard { GoldStandard::new( [42u8; 32], subject.to_string(), predicate.to_string(), expected_object.to_string(), 1000, "admin".to_string(), ) } #[tokio::test] async fn test_set_and_get_gold_standard() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let gs = create_gold_standard("Earth", "has_shape", "oblate_spheroid"); gs_store.set_gold_standard(&gs).await.expect("set"); let retrieved = gs_store .get_gold_standard("Earth", "has_shape") .await .expect("get") .expect("should exist"); assert_eq!(retrieved, gs); } #[tokio::test] async fn test_get_nonexistent_gold_standard() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let result = gs_store.get_gold_standard("NonExistent", "predicate").await.expect("get"); assert!(result.is_none()); } #[tokio::test] async fn test_list_gold_standards() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let gs1 = create_gold_standard("Earth", "has_shape", "oblate_spheroid"); let gs2 = create_gold_standard("Semaglutide", "treats_condition", "type_2_diabetes"); let gs3 = create_gold_standard("Earth", "has_moon", "Luna"); gs_store.set_gold_standard(&gs1).await.expect("set gs1"); gs_store.set_gold_standard(&gs2).await.expect("set gs2"); gs_store.set_gold_standard(&gs3).await.expect("set gs3"); let list = gs_store.list_gold_standards().await.expect("list"); assert_eq!(list.len(), 3); // Should be sorted by subject, then predicate assert_eq!(list[0].subject, "Earth"); assert_eq!(list[0].predicate, "has_moon"); assert_eq!(list[1].subject, "Earth"); assert_eq!(list[1].predicate, "has_shape"); assert_eq!(list[2].subject, "Semaglutide"); } #[tokio::test] async fn test_remove_gold_standard() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let gs = create_gold_standard("Earth", "has_shape", "oblate_spheroid"); gs_store.set_gold_standard(&gs).await.expect("set"); // Verify it exists let retrieved = gs_store.get_gold_standard("Earth", "has_shape").await.expect("get"); assert!(retrieved.is_some()); // Remove it let removed = gs_store.remove_gold_standard("Earth", "has_shape").await.expect("remove"); assert!(removed); // Verify it's gone let after_removal = gs_store.get_gold_standard("Earth", "has_shape").await.expect("get"); assert!(after_removal.is_none()); } #[tokio::test] async fn test_remove_nonexistent_gold_standard() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let removed = gs_store.remove_gold_standard("NonExistent", "predicate").await.expect("remove"); assert!(!removed); } #[tokio::test] async fn test_overwrite_gold_standard() { let store = Arc::new(HybridStore::open_temp().expect("store")); let gs_store = GenericGoldStandardStore::new(store); let gs1 = create_gold_standard("Earth", "has_shape", "sphere"); gs_store.set_gold_standard(&gs1).await.expect("set"); // Overwrite with more accurate answer let gs2 = create_gold_standard("Earth", "has_shape", "oblate_spheroid"); gs_store.set_gold_standard(&gs2).await.expect("set"); let retrieved = gs_store .get_gold_standard("Earth", "has_shape") .await .expect("get") .expect("should exist"); assert_eq!(retrieved.expected_object, "oblate_spheroid"); } }