Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
124
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/config.rs
vendored
Normal file
124
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/config.rs
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
//! Configuration for the claude-flow memory adapter.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rvf_runtime::options::DistanceMetric;
|
||||
|
||||
/// Configuration for the RVF-backed claude-flow memory store.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClaudeFlowConfig {
|
||||
/// Directory where RVF data files are stored.
|
||||
pub data_dir: PathBuf,
|
||||
/// Vector embedding dimension (must match the embeddings used by claude-flow).
|
||||
pub dimension: u16,
|
||||
/// Distance metric for similarity search.
|
||||
pub metric: DistanceMetric,
|
||||
/// Whether to record witness entries for audit trails.
|
||||
pub enable_witness: bool,
|
||||
}
|
||||
|
||||
impl ClaudeFlowConfig {
|
||||
/// Create a new configuration with required parameters.
|
||||
pub fn new(data_dir: impl Into<PathBuf>, dimension: u16) -> Self {
|
||||
Self {
|
||||
data_dir: data_dir.into(),
|
||||
dimension,
|
||||
metric: DistanceMetric::Cosine,
|
||||
enable_witness: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the distance metric.
|
||||
pub fn with_metric(mut self, metric: DistanceMetric) -> Self {
|
||||
self.metric = metric;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable witness audit trails.
|
||||
pub fn with_witness(mut self, enable: bool) -> Self {
|
||||
self.enable_witness = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the path to the main vector store RVF file.
|
||||
pub fn store_path(&self) -> PathBuf {
|
||||
self.data_dir.join("memory.rvf")
|
||||
}
|
||||
|
||||
/// Return the path to the witness chain file.
|
||||
pub fn witness_path(&self) -> PathBuf {
|
||||
self.data_dir.join("witness.bin")
|
||||
}
|
||||
|
||||
/// Ensure the data directory exists.
|
||||
pub fn ensure_dirs(&self) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(&self.data_dir)
|
||||
}
|
||||
|
||||
/// Validate the configuration.
|
||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||
if self.dimension == 0 {
|
||||
return Err(ConfigError::InvalidDimension);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors specific to adapter configuration.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConfigError {
|
||||
/// Dimension must be > 0.
|
||||
InvalidDimension,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidDimension => write!(f, "vector dimension must be > 0"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn config_defaults() {
|
||||
let cfg = ClaudeFlowConfig::new("/tmp/test", 384);
|
||||
assert_eq!(cfg.dimension, 384);
|
||||
assert_eq!(cfg.metric, DistanceMetric::Cosine);
|
||||
assert!(cfg.enable_witness);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_paths() {
|
||||
let cfg = ClaudeFlowConfig::new("/data/memory", 128);
|
||||
assert_eq!(cfg.store_path(), Path::new("/data/memory/memory.rvf"));
|
||||
assert_eq!(cfg.witness_path(), Path::new("/data/memory/witness.bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zero_dimension() {
|
||||
let cfg = ClaudeFlowConfig::new("/tmp", 0);
|
||||
assert_eq!(cfg.validate(), Err(ConfigError::InvalidDimension));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_ok() {
|
||||
let cfg = ClaudeFlowConfig::new("/tmp", 64);
|
||||
assert!(cfg.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_methods() {
|
||||
let cfg = ClaudeFlowConfig::new("/tmp", 256)
|
||||
.with_metric(DistanceMetric::L2)
|
||||
.with_witness(false);
|
||||
assert_eq!(cfg.metric, DistanceMetric::L2);
|
||||
assert!(!cfg.enable_witness);
|
||||
}
|
||||
}
|
||||
48
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/lib.rs
vendored
Normal file
48
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/lib.rs
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
//! RVF adapter for the claude-flow memory subsystem.
|
||||
//!
|
||||
//! This crate bridges claude-flow's key/value/embedding memory model
|
||||
//! with the RuVector Format (RVF) segment store. Memory entries are
|
||||
//! persisted as RVF files with the RVText profile, and every mutation
|
||||
//! is recorded in a WITNESS_SEG audit trail for tamper-evident logging.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! - **`RvfMemoryStore`**: Main API wrapping `RvfStore` for
|
||||
//! store/search/retrieve/delete operations on memory entries.
|
||||
//! - **`WitnessChain`**: Persistent, append-only audit log using
|
||||
//! `rvf_crypto::witness` chains (SHAKE-256 linked).
|
||||
//! - **`ClaudeFlowConfig`**: Configuration for data directory, embedding
|
||||
//! dimension, distance metric, and witness toggle.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use rvf_adapter_claude_flow::{ClaudeFlowConfig, RvfMemoryStore};
|
||||
//!
|
||||
//! let config = ClaudeFlowConfig::new("/tmp/claude-flow-memory", 384);
|
||||
//! let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
//!
|
||||
//! // Store a memory entry with its embedding
|
||||
//! let embedding = vec![0.1f32; 384];
|
||||
//! store.store_memory("auth-pattern", "JWT with refresh tokens",
|
||||
//! "patterns", &["auth".into()], &embedding).unwrap();
|
||||
//!
|
||||
//! // Search by embedding similarity
|
||||
//! let results = store.search_memory(&embedding, 5, Some("patterns"), None).unwrap();
|
||||
//!
|
||||
//! // Retrieve by key
|
||||
//! let id = store.retrieve_memory("auth-pattern", "patterns");
|
||||
//!
|
||||
//! // Delete
|
||||
//! store.delete_memory("auth-pattern", "patterns").unwrap();
|
||||
//!
|
||||
//! store.close().unwrap();
|
||||
//! ```
|
||||
|
||||
pub mod config;
|
||||
pub mod memory_store;
|
||||
pub mod witness;
|
||||
|
||||
pub use config::ClaudeFlowConfig;
|
||||
pub use memory_store::{MemoryEntry, MemoryStoreError, RvfMemoryStore};
|
||||
pub use witness::{WitnessChain, WitnessError};
|
||||
445
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/memory_store.rs
vendored
Normal file
445
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/memory_store.rs
vendored
Normal file
@@ -0,0 +1,445 @@
|
||||
//! `RvfMemoryStore` — wraps `RvfStore` for claude-flow memory operations.
|
||||
//!
|
||||
//! Maps claude-flow's key/value/namespace/tags/embedding model onto the
|
||||
//! RVF segment model:
|
||||
//! - Embeddings are stored as vectors via `ingest_batch`
|
||||
//! - Keys and namespaces are encoded as metadata (META_SEG fields)
|
||||
//! - Searches use `query` with optional namespace filtering
|
||||
//! - Deletes use soft-delete with witness recording
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rvf_runtime::filter::{FilterExpr, FilterValue};
|
||||
use rvf_runtime::options::{MetadataEntry, MetadataValue, QueryOptions, RvfOptions};
|
||||
use rvf_runtime::{RvfStore, SearchResult};
|
||||
use rvf_types::RvfError;
|
||||
|
||||
use crate::config::ClaudeFlowConfig;
|
||||
use crate::witness::WitnessChain;
|
||||
|
||||
/// Metadata field IDs for claude-flow memory entries.
|
||||
const FIELD_KEY: u16 = 0;
|
||||
const FIELD_NAMESPACE: u16 = 1;
|
||||
const FIELD_TAGS: u16 = 2;
|
||||
|
||||
/// A memory entry returned from retrieval or search.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MemoryEntry {
|
||||
/// The memory key.
|
||||
pub key: String,
|
||||
/// The namespace this entry belongs to.
|
||||
pub namespace: String,
|
||||
/// Tags associated with this entry.
|
||||
pub tags: Vec<String>,
|
||||
/// The vector ID in the underlying store.
|
||||
pub vector_id: u64,
|
||||
/// Distance from query (only meaningful for search results).
|
||||
pub distance: f32,
|
||||
}
|
||||
|
||||
/// The RVF-backed memory store for claude-flow.
|
||||
pub struct RvfMemoryStore {
|
||||
store: RvfStore,
|
||||
witness: Option<WitnessChain>,
|
||||
config: ClaudeFlowConfig,
|
||||
/// Maps "namespace/key" -> vector_id for fast lookup.
|
||||
key_index: HashMap<String, u64>,
|
||||
/// Next vector ID to assign.
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl RvfMemoryStore {
|
||||
/// Create a new memory store, initializing the data directory and RVF file.
|
||||
pub fn create(config: ClaudeFlowConfig) -> Result<Self, MemoryStoreError> {
|
||||
config.validate().map_err(MemoryStoreError::Config)?;
|
||||
config.ensure_dirs().map_err(|e| MemoryStoreError::Io(e.to_string()))?;
|
||||
|
||||
let rvf_options = RvfOptions {
|
||||
dimension: config.dimension,
|
||||
metric: config.metric,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let store = RvfStore::create(&config.store_path(), rvf_options)
|
||||
.map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
let witness = if config.enable_witness {
|
||||
Some(WitnessChain::create(&config.witness_path())
|
||||
.map_err(MemoryStoreError::Witness)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
witness,
|
||||
config,
|
||||
key_index: HashMap::new(),
|
||||
next_id: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open an existing memory store.
|
||||
pub fn open(config: ClaudeFlowConfig) -> Result<Self, MemoryStoreError> {
|
||||
config.validate().map_err(MemoryStoreError::Config)?;
|
||||
|
||||
let store = RvfStore::open(&config.store_path())
|
||||
.map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
let witness = if config.enable_witness {
|
||||
Some(WitnessChain::open_or_create(&config.witness_path())
|
||||
.map_err(MemoryStoreError::Witness)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Rebuild the key_index from the store status.
|
||||
// Since RvfStore doesn't expose metadata iteration, we start fresh.
|
||||
// Existing vectors remain searchable by embedding; key lookup is
|
||||
// rebuilt as entries are re-stored.
|
||||
let status = store.status();
|
||||
let next_id = status.total_vectors + status.current_epoch as u64 + 1;
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
witness,
|
||||
config,
|
||||
key_index: HashMap::new(),
|
||||
next_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a memory entry with its embedding vector.
|
||||
///
|
||||
/// If an entry with the same key and namespace already exists, the old
|
||||
/// one is soft-deleted and replaced.
|
||||
pub fn store_memory(
|
||||
&mut self,
|
||||
key: &str,
|
||||
_value: &str,
|
||||
namespace: &str,
|
||||
tags: &[String],
|
||||
embedding: &[f32],
|
||||
) -> Result<u64, MemoryStoreError> {
|
||||
if embedding.len() != self.config.dimension as usize {
|
||||
return Err(MemoryStoreError::DimensionMismatch {
|
||||
expected: self.config.dimension as usize,
|
||||
got: embedding.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// If key already exists in this namespace, soft-delete the old entry.
|
||||
let compound_key = format!("{namespace}/{key}");
|
||||
if let Some(&old_id) = self.key_index.get(&compound_key) {
|
||||
self.store.delete(&[old_id]).map_err(MemoryStoreError::Rvf)?;
|
||||
}
|
||||
|
||||
let vector_id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
// Encode tags as a comma-separated string for metadata storage.
|
||||
let tags_str = tags.join(",");
|
||||
|
||||
let metadata = vec![
|
||||
MetadataEntry { field_id: FIELD_KEY, value: MetadataValue::String(key.to_string()) },
|
||||
MetadataEntry { field_id: FIELD_NAMESPACE, value: MetadataValue::String(namespace.to_string()) },
|
||||
MetadataEntry { field_id: FIELD_TAGS, value: MetadataValue::String(tags_str) },
|
||||
];
|
||||
|
||||
self.store
|
||||
.ingest_batch(&[embedding], &[vector_id], Some(&metadata))
|
||||
.map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
self.key_index.insert(compound_key, vector_id);
|
||||
|
||||
if let Some(ref mut w) = self.witness {
|
||||
let _ = w.record_store(key, namespace);
|
||||
}
|
||||
|
||||
Ok(vector_id)
|
||||
}
|
||||
|
||||
/// Search memory by embedding vector, optionally filtering by namespace.
|
||||
pub fn search_memory(
|
||||
&mut self,
|
||||
query_embedding: &[f32],
|
||||
k: usize,
|
||||
namespace: Option<&str>,
|
||||
_threshold: Option<f32>,
|
||||
) -> Result<Vec<SearchResult>, MemoryStoreError> {
|
||||
if query_embedding.len() != self.config.dimension as usize {
|
||||
return Err(MemoryStoreError::DimensionMismatch {
|
||||
expected: self.config.dimension as usize,
|
||||
got: query_embedding.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let filter = namespace.map(|ns| {
|
||||
FilterExpr::Eq(FIELD_NAMESPACE, FilterValue::String(ns.to_string()))
|
||||
});
|
||||
|
||||
let options = QueryOptions {
|
||||
filter,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let results = self.store.query(query_embedding, k, &options)
|
||||
.map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
if let Some(ref mut w) = self.witness {
|
||||
let ns = namespace.unwrap_or("*");
|
||||
let _ = w.record_search(ns, k);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Retrieve a memory entry by key and namespace.
|
||||
///
|
||||
/// Returns the vector ID if found (the entry can then be used with
|
||||
/// the underlying store for further operations).
|
||||
pub fn retrieve_memory(
|
||||
&self,
|
||||
key: &str,
|
||||
namespace: &str,
|
||||
) -> Option<u64> {
|
||||
let compound_key = format!("{namespace}/{key}");
|
||||
self.key_index.get(&compound_key).copied()
|
||||
}
|
||||
|
||||
/// Soft-delete a memory entry by key and namespace.
|
||||
pub fn delete_memory(
|
||||
&mut self,
|
||||
key: &str,
|
||||
namespace: &str,
|
||||
) -> Result<bool, MemoryStoreError> {
|
||||
let compound_key = format!("{namespace}/{key}");
|
||||
if let Some(vector_id) = self.key_index.remove(&compound_key) {
|
||||
self.store.delete(&[vector_id]).map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
if let Some(ref mut w) = self.witness {
|
||||
let _ = w.record_delete(key, namespace);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run compaction on the underlying store.
|
||||
pub fn compact(&mut self) -> Result<(), MemoryStoreError> {
|
||||
self.store.compact().map_err(MemoryStoreError::Rvf)?;
|
||||
|
||||
if let Some(ref mut w) = self.witness {
|
||||
let _ = w.record_compact();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current store status.
|
||||
pub fn status(&self) -> rvf_runtime::StoreStatus {
|
||||
self.store.status()
|
||||
}
|
||||
|
||||
/// Return a reference to the witness chain (if enabled).
|
||||
pub fn witness(&self) -> Option<&WitnessChain> {
|
||||
self.witness.as_ref()
|
||||
}
|
||||
|
||||
/// Close the memory store, releasing locks.
|
||||
pub fn close(self) -> Result<(), MemoryStoreError> {
|
||||
self.store.close().map_err(MemoryStoreError::Rvf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from memory store operations.
|
||||
#[derive(Debug)]
|
||||
pub enum MemoryStoreError {
|
||||
/// Underlying RVF store error.
|
||||
Rvf(RvfError),
|
||||
/// Witness chain error.
|
||||
Witness(crate::witness::WitnessError),
|
||||
/// Configuration error.
|
||||
Config(crate::config::ConfigError),
|
||||
/// I/O error.
|
||||
Io(String),
|
||||
/// Embedding dimension mismatch.
|
||||
DimensionMismatch { expected: usize, got: usize },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MemoryStoreError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Rvf(e) => write!(f, "RVF store error: {e}"),
|
||||
Self::Witness(e) => write!(f, "witness error: {e}"),
|
||||
Self::Config(e) => write!(f, "config error: {e}"),
|
||||
Self::Io(msg) => write!(f, "I/O error: {msg}"),
|
||||
Self::DimensionMismatch { expected, got } => {
|
||||
write!(f, "dimension mismatch: expected {expected}, got {got}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MemoryStoreError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_config(dir: &Path) -> ClaudeFlowConfig {
|
||||
ClaudeFlowConfig::new(dir, 4)
|
||||
}
|
||||
|
||||
fn make_embedding(seed: f32) -> Vec<f32> {
|
||||
vec![seed, seed * 0.5, seed * 0.25, seed * 0.125]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_store() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
let id = store.store_memory(
|
||||
"key1", "value1", "default", &["tag1".into(), "tag2".into()],
|
||||
&make_embedding(1.0),
|
||||
).unwrap();
|
||||
assert!(id > 0);
|
||||
|
||||
let status = store.status();
|
||||
assert_eq!(status.total_vectors, 1);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_and_search() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
|
||||
store.store_memory("a", "val_a", "ns1", &[], &[1.0, 0.0, 0.0, 0.0]).unwrap();
|
||||
store.store_memory("b", "val_b", "ns1", &[], &[0.0, 1.0, 0.0, 0.0]).unwrap();
|
||||
store.store_memory("c", "val_c", "ns2", &[], &[0.0, 0.0, 1.0, 0.0]).unwrap();
|
||||
|
||||
// Search all namespaces
|
||||
let results = store.search_memory(&[1.0, 0.0, 0.0, 0.0], 3, None, None).unwrap();
|
||||
assert_eq!(results.len(), 3);
|
||||
|
||||
// Search filtered by namespace
|
||||
let results = store.search_memory(&[1.0, 0.0, 0.0, 0.0], 3, Some("ns1"), None).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieve_by_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
let id = store.store_memory("mykey", "myval", "ns", &[], &make_embedding(2.0)).unwrap();
|
||||
|
||||
assert_eq!(store.retrieve_memory("mykey", "ns"), Some(id));
|
||||
assert_eq!(store.retrieve_memory("missing", "ns"), None);
|
||||
assert_eq!(store.retrieve_memory("mykey", "other_ns"), None);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_memory() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
store.store_memory("k", "v", "ns", &[], &make_embedding(3.0)).unwrap();
|
||||
|
||||
assert!(store.delete_memory("k", "ns").unwrap());
|
||||
assert!(!store.delete_memory("k", "ns").unwrap()); // already deleted
|
||||
assert_eq!(store.retrieve_memory("k", "ns"), None);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_existing_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
let id1 = store.store_memory("k", "v1", "ns", &[], &make_embedding(1.0)).unwrap();
|
||||
let id2 = store.store_memory("k", "v2", "ns", &[], &make_embedding(2.0)).unwrap();
|
||||
|
||||
// New ID should be different (old was soft-deleted)
|
||||
assert_ne!(id1, id2);
|
||||
assert_eq!(store.retrieve_memory("k", "ns"), Some(id2));
|
||||
|
||||
// Only one live vector
|
||||
let status = store.status();
|
||||
assert_eq!(status.total_vectors, 1);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_mismatch() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
let result = store.store_memory("k", "v", "ns", &[], &[1.0, 2.0]); // dim=2 vs config dim=4
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_audit_trail() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
store.store_memory("a", "v", "ns", &[], &make_embedding(1.0)).unwrap();
|
||||
store.search_memory(&make_embedding(1.0), 1, None, None).unwrap();
|
||||
store.delete_memory("a", "ns").unwrap();
|
||||
|
||||
let witness = store.witness().unwrap();
|
||||
assert_eq!(witness.len(), 3); // store + search + delete
|
||||
assert_eq!(witness.verify().unwrap(), 3);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_works() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
|
||||
let mut store = RvfMemoryStore::create(config).unwrap();
|
||||
store.store_memory("a", "v", "ns", &[], &make_embedding(1.0)).unwrap();
|
||||
store.store_memory("b", "v", "ns", &[], &make_embedding(2.0)).unwrap();
|
||||
store.delete_memory("a", "ns").unwrap();
|
||||
store.compact().unwrap();
|
||||
|
||||
let status = store.status();
|
||||
assert_eq!(status.total_vectors, 1);
|
||||
|
||||
store.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_witness_when_disabled() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = ClaudeFlowConfig::new(dir.path(), 4).with_witness(false);
|
||||
|
||||
let store = RvfMemoryStore::create(config).unwrap();
|
||||
assert!(store.witness().is_none());
|
||||
store.close().unwrap();
|
||||
}
|
||||
}
|
||||
292
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/witness.rs
vendored
Normal file
292
vendor/ruvector/crates/rvf/rvf-adapters/claude-flow/src/witness.rs
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
//! Audit trail using WITNESS_SEG for claude-flow memory operations.
|
||||
//!
|
||||
//! Wraps `rvf_crypto::witness` to provide a persistent, append-only
|
||||
//! witness chain that records every memory store/delete/search action.
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rvf_crypto::witness::{WitnessEntry, create_witness_chain, verify_witness_chain};
|
||||
use rvf_crypto::shake256_256;
|
||||
|
||||
/// Witness type constants for claude-flow actions.
|
||||
pub const WITNESS_STORE: u8 = 0x01;
|
||||
pub const WITNESS_DELETE: u8 = 0x02;
|
||||
pub const WITNESS_SEARCH: u8 = 0x03;
|
||||
pub const WITNESS_COMPACT: u8 = 0x04;
|
||||
|
||||
/// Persistent witness chain that records memory operations.
|
||||
pub struct WitnessChain {
|
||||
path: PathBuf,
|
||||
/// Cached chain bytes (in-memory mirror of the file).
|
||||
chain_data: Vec<u8>,
|
||||
/// Number of entries in the chain.
|
||||
entry_count: usize,
|
||||
}
|
||||
|
||||
impl WitnessChain {
|
||||
/// Create a new (empty) witness chain file at the given path.
|
||||
pub fn create(path: &Path) -> Result<Self, WitnessError> {
|
||||
File::create(path).map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
chain_data: Vec::new(),
|
||||
entry_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open an existing witness chain file, verifying its integrity.
|
||||
pub fn open(path: &Path) -> Result<Self, WitnessError> {
|
||||
let mut file = File::open(path).map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data).map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
|
||||
if data.is_empty() {
|
||||
return Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
chain_data: Vec::new(),
|
||||
entry_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let entries = verify_witness_chain(&data)
|
||||
.map_err(|_| WitnessError::ChainCorrupted)?;
|
||||
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
chain_data: data,
|
||||
entry_count: entries.len(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open an existing chain or create a new one.
|
||||
pub fn open_or_create(path: &Path) -> Result<Self, WitnessError> {
|
||||
if path.exists() {
|
||||
Self::open(path)
|
||||
} else {
|
||||
Self::create(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a memory store action.
|
||||
pub fn record_store(&mut self, key: &str, namespace: &str) -> Result<(), WitnessError> {
|
||||
let mut hasher_input = Vec::new();
|
||||
hasher_input.extend_from_slice(b"store:");
|
||||
hasher_input.extend_from_slice(namespace.as_bytes());
|
||||
hasher_input.push(b'/');
|
||||
hasher_input.extend_from_slice(key.as_bytes());
|
||||
self.append_entry(&hasher_input, WITNESS_STORE)
|
||||
}
|
||||
|
||||
/// Record a memory delete action.
|
||||
pub fn record_delete(&mut self, key: &str, namespace: &str) -> Result<(), WitnessError> {
|
||||
let mut hasher_input = Vec::new();
|
||||
hasher_input.extend_from_slice(b"delete:");
|
||||
hasher_input.extend_from_slice(namespace.as_bytes());
|
||||
hasher_input.push(b'/');
|
||||
hasher_input.extend_from_slice(key.as_bytes());
|
||||
self.append_entry(&hasher_input, WITNESS_DELETE)
|
||||
}
|
||||
|
||||
/// Record a search action.
|
||||
pub fn record_search(&mut self, namespace: &str, k: usize) -> Result<(), WitnessError> {
|
||||
let mut hasher_input = Vec::new();
|
||||
hasher_input.extend_from_slice(b"search:");
|
||||
hasher_input.extend_from_slice(namespace.as_bytes());
|
||||
hasher_input.push(b':');
|
||||
hasher_input.extend_from_slice(k.to_string().as_bytes());
|
||||
self.append_entry(&hasher_input, WITNESS_SEARCH)
|
||||
}
|
||||
|
||||
/// Record a compaction action.
|
||||
pub fn record_compact(&mut self) -> Result<(), WitnessError> {
|
||||
self.append_entry(b"compact", WITNESS_COMPACT)
|
||||
}
|
||||
|
||||
/// Verify the entire chain is intact.
|
||||
pub fn verify(&self) -> Result<usize, WitnessError> {
|
||||
if self.chain_data.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let entries = verify_witness_chain(&self.chain_data)
|
||||
.map_err(|_| WitnessError::ChainCorrupted)?;
|
||||
Ok(entries.len())
|
||||
}
|
||||
|
||||
/// Return the number of entries in the chain.
|
||||
pub fn len(&self) -> usize {
|
||||
self.entry_count
|
||||
}
|
||||
|
||||
/// Return whether the chain is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entry_count == 0
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────────
|
||||
|
||||
fn append_entry(&mut self, action_data: &[u8], witness_type: u8) -> Result<(), WitnessError> {
|
||||
let action_hash = shake256_256(action_data);
|
||||
let timestamp_ns = now_ns();
|
||||
|
||||
let entry = WitnessEntry {
|
||||
prev_hash: [0u8; 32], // create_witness_chain will set this
|
||||
action_hash,
|
||||
timestamp_ns,
|
||||
witness_type,
|
||||
};
|
||||
|
||||
// Rebuild the entire chain with the new entry appended.
|
||||
// This is correct because create_witness_chain re-links prev_hash.
|
||||
let mut all_entries = if self.chain_data.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
verify_witness_chain(&self.chain_data)
|
||||
.map_err(|_| WitnessError::ChainCorrupted)?
|
||||
};
|
||||
all_entries.push(entry);
|
||||
|
||||
let new_chain = create_witness_chain(&all_entries);
|
||||
|
||||
// Persist atomically: write to temp then rename.
|
||||
let tmp_path = self.path.with_extension("bin.tmp");
|
||||
{
|
||||
let mut f = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&tmp_path)
|
||||
.map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
f.write_all(&new_chain).map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
f.sync_all().map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
}
|
||||
std::fs::rename(&tmp_path, &self.path).map_err(|e| WitnessError::Io(e.to_string()))?;
|
||||
|
||||
self.chain_data = new_chain;
|
||||
self.entry_count = all_entries.len();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from witness chain operations.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum WitnessError {
|
||||
/// I/O error (stringified for Clone/Eq compatibility).
|
||||
Io(String),
|
||||
/// Chain integrity verification failed.
|
||||
ChainCorrupted,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WitnessError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(msg) => write!(f, "witness I/O error: {msg}"),
|
||||
Self::ChainCorrupted => write!(f, "witness chain integrity check failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for WitnessError {}
|
||||
|
||||
fn now_ns() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn create_and_open_empty() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
let chain = WitnessChain::create(&path).unwrap();
|
||||
assert_eq!(chain.len(), 0);
|
||||
assert!(chain.is_empty());
|
||||
|
||||
let reopened = WitnessChain::open(&path).unwrap();
|
||||
assert_eq!(reopened.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_and_verify() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
let mut chain = WitnessChain::create(&path).unwrap();
|
||||
chain.record_store("key1", "default").unwrap();
|
||||
chain.record_search("default", 5).unwrap();
|
||||
chain.record_delete("key1", "default").unwrap();
|
||||
assert_eq!(chain.len(), 3);
|
||||
|
||||
let count = chain.verify().unwrap();
|
||||
assert_eq!(count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_across_reopen() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
{
|
||||
let mut chain = WitnessChain::create(&path).unwrap();
|
||||
chain.record_store("a", "ns").unwrap();
|
||||
chain.record_store("b", "ns").unwrap();
|
||||
}
|
||||
|
||||
let chain = WitnessChain::open(&path).unwrap();
|
||||
assert_eq!(chain.len(), 2);
|
||||
assert_eq!(chain.verify().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tampered_chain_detected() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
{
|
||||
let mut chain = WitnessChain::create(&path).unwrap();
|
||||
chain.record_store("x", "ns").unwrap();
|
||||
chain.record_store("y", "ns").unwrap();
|
||||
}
|
||||
|
||||
// Tamper with the file
|
||||
let mut data = std::fs::read(&path).unwrap();
|
||||
if data.len() > 40 {
|
||||
data[40] ^= 0xFF;
|
||||
}
|
||||
std::fs::write(&path, &data).unwrap();
|
||||
|
||||
let result = WitnessChain::open(&path);
|
||||
assert!(result.is_err() || result.unwrap().verify().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_or_create_new() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
let chain = WitnessChain::open_or_create(&path).unwrap();
|
||||
assert!(chain.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_or_create_existing() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("witness.bin");
|
||||
|
||||
{
|
||||
let mut chain = WitnessChain::create(&path).unwrap();
|
||||
chain.record_compact().unwrap();
|
||||
}
|
||||
|
||||
let chain = WitnessChain::open_or_create(&path).unwrap();
|
||||
assert_eq!(chain.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user