use super::config::DreamConfig;
use crate::memory_core::retrieval::PalaceHandle;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::Path;
pub(super) const FILE_NAME: &str = "dream_settled.json";
const FINGERPRINT_DOMAIN: &[u8] = b"trusty-memory/dream-settled/v1";
#[derive(Debug, Serialize, Deserialize)]
struct SettledMarker {
fingerprint: String,
recorded_at: chrono::DateTime<chrono::Utc>,
}
pub(super) fn corpus_fingerprint(handle: &PalaceHandle, config: &DreamConfig) -> String {
let mut entries: Vec<(uuid::Uuid, bool, [u8; 32])> = {
let drawers = handle.drawers.read();
drawers
.iter()
.map(|d| {
let stored = d.content_hash();
let mut digest = *stored.as_bytes();
if stored.is_unset() {
digest.copy_from_slice(&Sha256::digest(d.content().as_bytes()));
}
(d.id, d.drawer_type.is_protected(), digest)
})
.collect()
};
entries.sort_unstable_by_key(|(id, _, _)| *id);
let mut hasher = Sha256::new();
hasher.update(FINGERPRINT_DOMAIN);
hasher.update(config.dedup_threshold.to_bits().to_le_bytes());
hasher.update([u8::from(config.semantic.enabled)]);
let model = config.semantic.model.as_bytes();
hasher.update((model.len() as u64).to_le_bytes());
hasher.update(model);
for (id, protected, digest) in &entries {
hasher.update(id.as_bytes());
hasher.update([u8::from(*protected)]);
hasher.update(digest);
}
hex::encode(hasher.finalize())
}
pub(super) fn is_settled(data_dir: &Path, fingerprint: &str) -> bool {
match load(data_dir) {
Ok(Some(marker)) => marker.fingerprint == fingerprint,
Ok(None) => false,
Err(e) => {
tracing::warn!(
dir = %data_dir.display(),
"dream settled marker unreadable; running the full cycle: {e:#}"
);
false
}
}
}
pub(super) fn record_settled(data_dir: &Path, fingerprint: &str) -> Result<()> {
let marker = SettledMarker {
fingerprint: fingerprint.to_string(),
recorded_at: chrono::Utc::now(),
};
let raw = serde_json::to_string_pretty(&marker).context("serialize dream settled marker")?;
let path = data_dir.join(FILE_NAME);
crate::atomic_file::write_atomic(&path, raw.as_bytes())
.with_context(|| format!("write {}", path.display()))
}
fn load(data_dir: &Path) -> Result<Option<SettledMarker>> {
let path = data_dir.join(FILE_NAME);
let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
};
let marker = serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
Ok(Some(marker))
}