Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
[package]
name = "rvf-adapter-claude-flow"
version = "0.1.0"
edition = "2021"
description = "RVF adapter for claude-flow memory subsystem — stores memory entries as RVF files with WITNESS_SEG audit trails"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/ruvector"
[features]
default = ["std"]
std = []
[dependencies]
rvf-types = { path = "../../rvf-types", features = ["std"] }
rvf-runtime = { path = "../../rvf-runtime", features = ["std"] }
rvf-crypto = { path = "../../rvf-crypto", features = ["std"] }
[dev-dependencies]
tempfile = "3"

View 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);
}
}

View 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};

View 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();
}
}

View 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);
}
}