velesdb-memory 0.14.1

VelesDB-memory: local-first MCP memory server for AI agents (remember/recall/relate/forget/why + deterministic context compiler).
Documentation
use std::fmt::Write as _;
use std::path::{Path, PathBuf};

use crate::MemoryError;

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct EpochIdentity {
    source_path: PathBuf,
    source_provenance: String,
    target_model: String,
    target_dimension: usize,
    target_witness: String,
    destination_path: PathBuf,
    epoch_id: String,
}

pub(crate) struct CutoverIdentity<'a> {
    pub(crate) source: &'a Path,
    pub(crate) destination: &'a Path,
    pub(crate) source_provenance: &'a str,
    pub(crate) target_model: &'a str,
    pub(crate) target_dimension: usize,
    pub(crate) target_witness: &'a str,
    pub(crate) epoch_id: &'a str,
}

impl EpochIdentity {
    pub(crate) fn source_path(&self) -> &Path {
        &self.source_path
    }

    pub(crate) fn source_provenance(&self) -> &str {
        &self.source_provenance
    }

    pub(crate) fn target_model(&self) -> &str {
        &self.target_model
    }

    pub(crate) fn target_dimension(&self) -> usize {
        self.target_dimension
    }

    pub(crate) fn target_witness(&self) -> &str {
        &self.target_witness
    }

    pub(crate) fn destination_path(&self) -> &Path {
        &self.destination_path
    }

    pub(crate) fn epoch_id(&self) -> &str {
        &self.epoch_id
    }

    pub(crate) fn new(
        source_path: PathBuf,
        source_provenance: String,
        target_model: String,
        target_dimension: usize,
        target_witness: String,
        destination_path: PathBuf,
    ) -> Result<Self, MemoryError> {
        let mut random = [0_u8; 16];
        getrandom::fill(&mut random)
            .map_err(|err| capture(format!("cannot mint epoch id: {err}")))?;
        let epoch_id = random
            .iter()
            .fold(String::with_capacity(32), |mut id, byte| {
                let _ = write!(id, "{byte:02x}");
                id
            });
        Self::validated(
            source_path,
            source_provenance,
            target_model,
            target_dimension,
            target_witness,
            destination_path,
            epoch_id,
        )
    }

    pub(crate) fn validate(&self) -> Result<(), MemoryError> {
        Self::validated(
            self.source_path.clone(),
            self.source_provenance.clone(),
            self.target_model.clone(),
            self.target_dimension,
            self.target_witness.clone(),
            self.destination_path.clone(),
            self.epoch_id.clone(),
        )
        .map(|_| ())
    }

    #[cfg(test)]
    pub(crate) fn for_test(
        source_path: PathBuf,
        source_provenance: &str,
        target_model: &str,
        target_dimension: usize,
        target_witness: &str,
        destination_path: PathBuf,
        epoch_id: &str,
    ) -> Self {
        Self::validated(
            source_path,
            source_provenance.to_owned(),
            target_model.to_owned(),
            target_dimension,
            target_witness.to_owned(),
            destination_path,
            epoch_id.to_owned(),
        )
        .expect("valid test epoch")
    }

    fn validated(
        source_path: PathBuf,
        source_provenance: String,
        target_model: String,
        target_dimension: usize,
        target_witness: String,
        destination_path: PathBuf,
        epoch_id: String,
    ) -> Result<Self, MemoryError> {
        validate_names_and_paths(
            &source_path,
            &source_provenance,
            &target_model,
            target_dimension,
            &destination_path,
        )?;
        validate_witness(&target_witness)?;
        validate_epoch_id(&epoch_id)?;
        Ok(Self {
            source_path,
            source_provenance,
            target_model,
            target_dimension,
            target_witness,
            destination_path,
            epoch_id,
        })
    }
}

fn validate_names_and_paths(
    source: &Path,
    source_provenance: &str,
    target_model: &str,
    target_dimension: usize,
    destination: &Path,
) -> Result<(), MemoryError> {
    if source_provenance.trim().is_empty() || target_model.trim().is_empty() {
        return Err(capture(
            "epoch provenance and target model must not be empty",
        ));
    }
    if target_dimension == 0 || source == destination {
        return Err(capture(
            "epoch paths must differ and target dimension must be positive",
        ));
    }
    Ok(())
}

fn validate_witness(witness: &str) -> Result<(), MemoryError> {
    let digest = witness
        .strip_prefix("sha256:")
        .ok_or_else(|| capture("target witness must use sha256"))?;
    if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(capture("target witness must contain 64 hexadecimal digits"));
    }
    Ok(())
}

pub(super) fn validate_epoch_id(epoch_id: &str) -> Result<(), MemoryError> {
    if epoch_id.len() != 32 || !epoch_id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(capture("epoch id must be 32 hexadecimal characters"));
    }
    Ok(())
}

fn capture(message: impl Into<String>) -> MemoryError {
    MemoryError::MigrationCapture(message.into())
}