use std::path::Path;
pub const PROVENANCE_FILE: &str = "embedding-provenance.json";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EmbeddingProvenance {
pub model: String,
pub dimension: usize,
}
impl EmbeddingProvenance {
#[must_use]
pub fn new(model: impl Into<String>, dimension: usize) -> Self {
Self {
model: model.into(),
dimension,
}
}
}
pub fn read(store_dir: &Path) -> Result<Option<EmbeddingProvenance>, String> {
let path = store_dir.join(PROVENANCE_FILE);
let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(format!("cannot read {PROVENANCE_FILE}: {err}")),
};
serde_json::from_str(&raw)
.map(Some)
.map_err(|err| format!("cannot parse {PROVENANCE_FILE}: {err} — delete it to reset the embedding record (the store's own data is untouched)"))
}
pub fn write(store_dir: &Path, provenance: &EmbeddingProvenance) -> Result<(), String> {
let body = serde_json::to_string_pretty(provenance)
.map_err(|err| format!("cannot serialise the embedding record: {err}"))?;
std::fs::write(store_dir.join(PROVENANCE_FILE), body)
.map_err(|err| format!("cannot write {PROVENANCE_FILE}: {err}"))
}
pub fn check(
stored: Option<&EmbeddingProvenance>,
model: &str,
dimension: usize,
) -> Result<(), String> {
let Some(stored) = stored else {
return Ok(());
};
if stored.model == model && stored.dimension == dimension {
return Ok(());
}
Err(format!(
"this store was filled with the embedding model '{}' ({} dimensions), and the daemon is \
configured for '{}' ({} dimensions). Vectors from two different models are not \
comparable, so recall would silently return nonsense. Either point \
VELESDB_MEMORY_EMBEDDER_MODEL back at '{}', or migrate the store against the new model \
with `velesdb-memory migrate-embeddings` (start with --dry-run; see #1762). Which \
backend serves the model does not matter and is not recorded: the same model over \
Ollama, oMLX or an OpenAI-compatible API produces the same vectors.",
stored.model, stored.dimension, model, dimension, stored.model
))
}
#[must_use]
pub fn unrecorded_model_note(model: &str) -> String {
format!(
"[velesdb-memory] this store predates embedding-model recording, so only the vector \
dimension could be compared against '{model}' — not the model itself. Two different \
models of the same width would pass this check. The record is written only for a store \
with no facts in it, never over existing data, because a wrong stamp would be trusted \
forever."
)
}
#[cfg(test)]
#[path = "embedding_provenance_tests.rs"]
mod tests;