use std::path::Path;
use crate::graph::dir_graph::DirGraph;
use crate::graph::embedder::Embedder;
use crate::okf::build::{build, effective_options, BuildOutput};
use crate::okf::model::BuildOptions;
use crate::okf::walk;
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fold(state: u64, bytes: &[u8]) -> u64 {
let mut hash = state;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn fold_entry(state: u64, rel_path: &str, size: u64, mtime: Option<i64>) -> u64 {
let mut hash = fold(state, &(rel_path.len() as u64).to_le_bytes());
hash = fold(hash, rel_path.as_bytes());
hash = fold(hash, &size.to_le_bytes());
fold(hash, &mtime.unwrap_or(i64::MIN).to_le_bytes())
}
pub fn fingerprint(root: &Path, opts: &BuildOptions) -> Result<u64, String> {
let mut warnings = Vec::new();
let (effective, _config) = effective_options(root, opts, &mut warnings)?;
let walked = walk::discover(root, &effective)?;
Ok(fingerprint_of(root, &walked, &effective))
}
pub(crate) fn fingerprint_of(root: &Path, walked: &walk::WalkResult, opts: &BuildOptions) -> u64 {
let mut hash = FNV_OFFSET;
for file in walked.concepts.iter().chain(walked.diverted.iter()) {
hash = fold_entry(hash, &file.rel_path, file.size, file.mtime);
}
for attachment in &walked.attachments {
hash = fold_entry(
hash,
&attachment.rel_path,
attachment.size,
attachment.mtime,
);
}
if opts.dialect == crate::okf::Dialect::Obsidian {
for (rel_path, size, mtime) in kglite_dir_entries(root) {
hash = fold_entry(hash, &rel_path, size, mtime);
}
}
hash
}
fn kglite_dir_entries(root: &Path) -> Vec<(String, u64, Option<i64>)> {
let dir = crate::okf::vault_config::config_dir(root);
let mut entries: Vec<(String, u64, Option<i64>)> = walkdir::WalkDir::new(&dir)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_file())
.filter_map(|entry| {
let rel = entry.path().strip_prefix(root).ok()?;
let rel_path = rel
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect::<Vec<_>>()
.join("/");
let meta = entry.metadata().ok();
Some((
rel_path,
meta.as_ref().map(|m| m.len()).unwrap_or(0),
meta.as_ref().and_then(walk::mtime_secs),
))
})
.collect();
entries.sort();
entries
}
pub fn rebuild_if_changed(
old: &DirGraph,
opts: &BuildOptions,
embedder: Option<&dyn Embedder>,
) -> Result<Option<BuildOutput>, String> {
let Some(root) = old.source_root.clone() else {
return Err(
"this graph carries no source_root: it was not built by okf::build, or it was \
saved by a version that did not record one. Build the directory with okf::build \
instead."
.to_string(),
);
};
let root = Path::new(&root);
if old.source_fingerprint == Some(fingerprint(root, opts)?) {
return Ok(None);
}
let mut out = build(root, opts)?;
let graph = crate::graph::handle::make_dir_graph_mut(&mut out.graph);
let (stores, _vectors, _skipped) = graph.copy_embeddings_from(old);
if let Some(model) = embedder {
let hooks = crate::graph::embeddings::EmbedHooks::default();
for (label, property) in out.report.embed_targets.clone() {
if let Err(error) = crate::graph::embeddings::embed_property(
&mut out.graph,
&label,
&property,
crate::graph::embeddings::EmbedMode::Changed,
model,
&hooks,
) {
out.report
.warnings
.push(format!("embed target `{label}.{property}` failed: {error}"));
}
}
} else if !out.report.embed_targets.is_empty() {
out.report.warnings.push(format!(
"`.kglite/vault.yaml` declares {} embed target(s) but no embedder was given — \
no vectors were computed",
out.report.embed_targets.len()
));
}
let _ = stores;
Ok(Some(out))
}
#[cfg(test)]
#[path = "fingerprint_tests.rs"]
mod tests;