#![allow(dead_code)]
#[path = "corpus_subset.rs"]
mod corpus_subset;
#[allow(unused_imports)]
pub use corpus_subset::{
extract_ground_truth_queries, load_chain_docs, load_chains_or_skip, load_subset_or_skip,
repo_root, salient_word, Chain, Doc, IRQuery, VaryingEmbedder, CORPUS_DIM, VECTOR_KIND,
};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::Engine;
use rusqlite::Connection;
use sha2::{Digest, Sha256};
use tempfile::TempDir;
type VecCache = HashMap<[u8; 32], Vec<f32>>;
const CACHE_FORMAT_VERSION: u32 = 1;
const TOOL_VERSION: &str = "corpus-harness/1";
const SYNTHETIC_REVISION: &str = "corpus-pack-4";
#[derive(Clone, Debug)]
pub struct IngestReport {
pub nodes: usize,
pub edges: usize,
pub edges_by_relation: BTreeMap<String, usize>,
pub embed_cache_hit: bool,
pub embedded_live: usize,
pub cache_miss_reason: Option<String>,
pub cache_path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct HeldOutQuery {
pub text: String,
pub target_doc_id: String,
pub target_body: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EmbedderKind {
Synthetic,
Real,
}
pub struct CorpusFixture {
label: String,
docs: Vec<Doc>,
corpus_absent: bool,
kind: EmbedderKind,
synthetic_revision: String,
real_unavailable: bool,
cache_dir_override: Option<PathBuf>,
embedder: OnceLock<Arc<CachingEmbedder>>,
}
impl CorpusFixture {
pub fn small() -> Self {
Self::global("small", 100)
}
pub fn medium() -> Self {
Self::global("medium", 1000)
}
pub fn full() -> Self {
Self::global("full", usize::MAX)
}
pub fn per_source(per_source: usize) -> Self {
match load_subset_or_skip(per_source) {
Some(docs) => Self::from_resolved(format!("per_source_{per_source}"), docs),
None => Self::absent(format!("per_source_{per_source}")),
}
}
pub fn from_docs(label: impl Into<String>, docs: Vec<Doc>) -> Self {
Self::from_resolved(label.into(), docs)
}
fn global(label: &str, take: usize) -> Self {
match load_subset_or_skip(usize::MAX) {
Some(mut docs) => {
docs.sort_by(|a, b| a.doc_id.cmp(&b.doc_id));
docs.dedup_by(|a, b| a.doc_id == b.doc_id);
docs.truncate(take);
Self::from_resolved(label.to_string(), docs)
}
None => Self::absent(label.to_string()),
}
}
fn from_resolved(label: String, docs: Vec<Doc>) -> Self {
Self {
label,
docs,
corpus_absent: false,
kind: EmbedderKind::Synthetic,
synthetic_revision: SYNTHETIC_REVISION.to_string(),
real_unavailable: false,
cache_dir_override: None,
embedder: OnceLock::new(),
}
}
fn absent(label: String) -> Self {
Self {
label,
docs: Vec::new(),
corpus_absent: true,
kind: EmbedderKind::Synthetic,
synthetic_revision: SYNTHETIC_REVISION.to_string(),
real_unavailable: false,
cache_dir_override: None,
embedder: OnceLock::new(),
}
}
#[must_use]
pub fn with_real_embedder(mut self) -> Self {
self.kind = EmbedderKind::Real;
self.real_unavailable = !cfg!(feature = "default-embedder");
self.embedder = OnceLock::new();
self
}
#[must_use]
pub fn with_synthetic_embedder(mut self) -> Self {
self.kind = EmbedderKind::Synthetic;
self.real_unavailable = false;
self.embedder = OnceLock::new();
self
}
#[must_use]
pub fn with_synthetic_revision(mut self, revision: impl Into<String>) -> Self {
self.kind = EmbedderKind::Synthetic;
self.real_unavailable = false;
self.synthetic_revision = revision.into();
self.embedder = OnceLock::new();
self
}
#[must_use]
pub fn with_cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.cache_dir_override = Some(dir.into());
self.embedder = OnceLock::new();
self
}
pub fn docs(&self) -> &[Doc] {
&self.docs
}
pub fn skip_reason(&self) -> Option<String> {
if self.corpus_absent {
return Some(format!(
"corpus not present on disk (label={}) — run tests/corpus/scripts/acquire_*.py first",
self.label
));
}
if self.real_unavailable {
return Some(format!(
"with_real_embedder() requires the `default-embedder` feature (label={})",
self.label
));
}
if self.docs.is_empty() {
return Some(format!("fixture resolved 0 docs (label={})", self.label));
}
None
}
pub fn open_engine(&self) -> (TempDir, Engine) {
assert!(
self.skip_reason().is_none(),
"open_engine on unavailable fixture: {:?}",
self.skip_reason()
);
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("corpus.sqlite");
let embedder = self.ensure_embedder();
let opened = Engine::open_with_embedder_for_test(&path, embedder as Arc<dyn Embedder>)
.expect("open engine with corpus-harness embedder");
opened.engine.configure_vector_kind_for_test(VECTOR_KIND).expect("configure vector kind");
(dir, opened.engine)
}
pub fn open_or_skip(&self) -> Option<(TempDir, Engine)> {
if let Some(reason) = self.skip_reason() {
eprintln!("SKIP: {reason}");
return None;
}
Some(self.open_engine())
}
pub fn ingest_into(&self, engine: &Engine) -> IngestReport {
let embedder = self.ensure_embedder();
let (nodes, edges, edges_by_relation) = corpus_subset::ingest(engine, &self.docs);
let (cache_hit, live) = embedder.persist();
IngestReport {
nodes,
edges,
edges_by_relation,
embed_cache_hit: cache_hit,
embedded_live: live,
cache_miss_reason: embedder.miss_reason.clone(),
cache_path: embedder.cache_path.clone(),
}
}
pub fn query_set(&self, n: usize, seed: u64) -> Vec<HeldOutQuery> {
if self.docs.is_empty() {
return Vec::new();
}
let mut indices: Vec<usize> = (0..self.docs.len()).collect();
let mut rng = SplitMix64::new(seed);
for i in (1..indices.len()).rev() {
let j = rng.next_in(i + 1);
indices.swap(i, j);
}
let mut out = Vec::with_capacity(n.min(self.docs.len()));
for &idx in &indices {
if out.len() >= n {
break;
}
let doc = &self.docs[idx];
if let Some(text) = synth_query(doc) {
out.push(HeldOutQuery {
text,
target_doc_id: doc.doc_id.clone(),
target_body: doc.body.clone(),
});
}
}
out
}
pub fn assert_vec0_row_count_matches_ingest(&self, engine: &Engine) {
engine.drain(15_000).expect("drain before vec0 count");
let conn = open_readonly(engine.path());
let vec_count: i64 = conn
.query_row("SELECT count(*) FROM vector_default", [], |row| row.get(0))
.expect("count vector_default");
let expected = self.docs.iter().filter(|d| !d.body.trim().is_empty()).count() as i64;
assert!(
vec_count >= expected,
"vector_default has {vec_count} rows but {expected} non-empty-body docs were ingested \
— projection apparently dropped some; vector path is NOT wired end-to-end"
);
}
pub fn assert_fts_index_populated(&self, engine: &Engine) {
engine.drain(15_000).expect("drain before fts count");
let conn = open_readonly(engine.path());
let fts_count: i64 = conn
.query_row("SELECT count(*) FROM search_index", [], |row| row.get(0))
.expect("count search_index");
assert!(fts_count > 0, "search_index is empty after ingest — FTS path is NOT wired");
}
pub fn assert_search_returns_non_empty_for_each(&self, engine: &Engine, qs: &[HeldOutQuery]) {
assert!(!qs.is_empty(), "assert_search_returns_non_empty_for_each: empty query set");
for q in qs {
let result = engine
.search(&q.text)
.unwrap_or_else(|e| panic!("search failed for query {:?}: {e:?}", q.target_doc_id));
assert!(
!result.results.is_empty(),
"engine.search returned empty for query from doc {} (text={:?})",
q.target_doc_id,
q.text
);
}
}
fn ensure_embedder(&self) -> Arc<CachingEmbedder> {
self.embedder
.get_or_init(|| {
let inner: Arc<dyn Embedder> = self.build_inner_embedder();
let identity = inner.identity();
let cache_dir = self.resolve_cache_dir();
Arc::new(CachingEmbedder::load(inner, identity, cache_dir, &self.label, &self.docs))
})
.clone()
}
fn build_inner_embedder(&self) -> Arc<dyn Embedder> {
match self.kind {
EmbedderKind::Synthetic => Arc::new(VaryingEmbedder::with_identity(
"varying",
&self.synthetic_revision,
CORPUS_DIM,
)),
EmbedderKind::Real => build_real_embedder(),
}
}
fn resolve_cache_dir(&self) -> PathBuf {
if let Some(dir) = &self.cache_dir_override {
return dir.clone();
}
if let Ok(env_dir) = std::env::var("FATHOMDB_CORPUS_CACHE_DIR") {
if !env_dir.is_empty() {
return PathBuf::from(env_dir);
}
}
let root = repo_root().unwrap_or_else(|| PathBuf::from("."));
root.join("data/corpus-data/.cache/embeddings")
}
}
#[cfg(feature = "default-embedder")]
fn build_real_embedder() -> Arc<dyn Embedder> {
Arc::new(fathomdb_embedder::CandleBgeEmbedder::new().expect("construct real bge embedder"))
}
#[cfg(not(feature = "default-embedder"))]
fn build_real_embedder() -> Arc<dyn Embedder> {
unreachable!("with_real_embedder is gated off via skip_reason when default-embedder is absent");
}
struct CachingEmbedder {
inner: Arc<dyn Embedder>,
identity: EmbedderIdentity,
cache: RwLock<VecCache>,
cache_path: PathBuf,
meta_path: PathBuf,
doc_manifest_sha: String,
subset_label: String,
loaded_from_disk: bool,
miss_reason: Option<String>,
live_misses: AtomicUsize,
}
impl CachingEmbedder {
fn load(
inner: Arc<dyn Embedder>,
identity: EmbedderIdentity,
cache_dir: PathBuf,
subset_label: &str,
docs: &[Doc],
) -> Self {
let doc_manifest_sha = doc_manifest_sha(docs);
let key = cache_key(&identity, subset_label, &doc_manifest_sha);
let cache_path = cache_dir.join(format!("{key}.bin"));
let meta_path = cache_dir.join(format!("{key}.meta.json"));
let (cache, loaded, miss_reason) =
match read_cache(&cache_path, &meta_path, &identity, &doc_manifest_sha) {
Ok(Some(entries)) => (entries, true, None),
Ok(None) => (HashMap::new(), false, Some("cold".to_string())),
Err(reason) => (HashMap::new(), false, Some(reason)),
};
if let Some(reason) = &miss_reason {
eprintln!(
"CORPUS_CACHE_MISS reason={reason} label={subset_label} path={}",
cache_path.display()
);
}
Self {
inner,
identity,
cache: RwLock::new(cache),
cache_path,
meta_path,
doc_manifest_sha,
subset_label: subset_label.to_string(),
loaded_from_disk: loaded,
miss_reason,
live_misses: AtomicUsize::new(0),
}
}
fn persist(&self) -> (bool, usize) {
let live = self.live_misses.load(Ordering::Relaxed);
if self.loaded_from_disk && live == 0 {
return (true, 0);
}
if let Err(e) = self.write_cache() {
eprintln!(
"CORPUS_CACHE_WRITE_FAIL reason={e} label={} path={}",
self.subset_label,
self.cache_path.display()
);
}
(self.loaded_from_disk, live)
}
fn write_cache(&self) -> Result<(), String> {
if let Some(parent) = self.cache_path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
let dim = self.identity.dimension as usize;
let guard = self.cache.read().expect("cache read lock");
let mut keys: Vec<&[u8; 32]> = guard.keys().collect();
keys.sort_unstable();
let mut blob: Vec<u8> = Vec::with_capacity(12 + keys.len() * (32 + dim * 4));
blob.extend_from_slice(&CACHE_FORMAT_VERSION.to_le_bytes());
blob.extend_from_slice(&(self.identity.dimension).to_le_bytes());
blob.extend_from_slice(&(keys.len() as u32).to_le_bytes());
for k in &keys {
let v = &guard[*k];
debug_assert_eq!(v.len(), dim, "cached vector dim mismatch");
blob.extend_from_slice(&k[..]);
for f in v {
blob.extend_from_slice(&f.to_le_bytes());
}
}
let entry_count = keys.len();
drop(guard);
let meta = serde_json::json!({
"format_version": CACHE_FORMAT_VERSION,
"tool_version": TOOL_VERSION,
"identity": {
"name": self.identity.name,
"revision": self.identity.revision,
"dimension": self.identity.dimension,
},
"subset_label": self.subset_label,
"doc_manifest_sha": self.doc_manifest_sha,
"dim": self.identity.dimension,
"entry_count": entry_count,
});
let meta_bytes = serde_json::to_vec_pretty(&meta).map_err(|e| e.to_string())?;
atomic_write(&self.cache_path, &blob)?;
atomic_write(&self.meta_path, &meta_bytes)?;
Ok(())
}
}
impl Embedder for CachingEmbedder {
fn identity(&self) -> EmbedderIdentity {
self.identity.clone()
}
fn embed(&self, input: &str) -> Result<Vector, EmbedderError> {
let key = text_sha256(input);
if let Some(v) = self.cache.read().expect("cache read lock").get(&key) {
return Ok(v.clone());
}
let v = self.inner.embed(input)?;
self.live_misses.fetch_add(1, Ordering::Relaxed);
self.cache.write().expect("cache write lock").insert(key, v.clone());
Ok(v)
}
}
fn text_sha256(text: &str) -> [u8; 32] {
let mut h = Sha256::new();
h.update(text.as_bytes());
h.finalize().into()
}
fn doc_manifest_sha(docs: &[Doc]) -> String {
let mut ids: Vec<&str> = docs.iter().map(|d| d.doc_id.as_str()).collect();
ids.sort_unstable();
ids.dedup();
let mut h = Sha256::new();
for id in ids {
h.update(id.as_bytes());
h.update(b"\n");
}
hex(&h.finalize())
}
fn cache_key(identity: &EmbedderIdentity, label: &str, doc_manifest_sha: &str) -> String {
let mut h = Sha256::new();
h.update(identity.name.as_bytes());
h.update(b"\0");
h.update(identity.revision.as_bytes());
h.update(b"\0");
h.update(identity.dimension.to_le_bytes());
h.update(b"\0");
h.update(label.as_bytes());
h.update(b"\0");
h.update(doc_manifest_sha.as_bytes());
hex(&h.finalize())
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn read_cache(
cache_path: &Path,
meta_path: &Path,
identity: &EmbedderIdentity,
doc_manifest_sha: &str,
) -> Result<Option<VecCache>, String> {
let blob_exists = cache_path.exists();
let meta_exists = meta_path.exists();
if !blob_exists && !meta_exists {
return Ok(None);
}
if blob_exists != meta_exists {
return Err(format!(
"partial cache (blob={blob_exists}, sidecar={meta_exists}) — treating as miss"
));
}
let meta_bytes = fs::read(meta_path).map_err(|e| format!("read meta: {e}"))?;
let meta: serde_json::Value =
serde_json::from_slice(&meta_bytes).map_err(|e| format!("parse meta: {e}"))?;
let fmt = meta.get("format_version").and_then(serde_json::Value::as_u64);
if fmt != Some(u64::from(CACHE_FORMAT_VERSION)) {
return Err(format!("format_version mismatch (got {fmt:?})"));
}
let m_name = meta.pointer("/identity/name").and_then(serde_json::Value::as_str);
let m_rev = meta.pointer("/identity/revision").and_then(serde_json::Value::as_str);
let m_dim = meta.pointer("/identity/dimension").and_then(serde_json::Value::as_u64);
if m_name != Some(identity.name.as_str())
|| m_rev != Some(identity.revision.as_str())
|| m_dim != Some(u64::from(identity.dimension))
{
return Err("identity mismatch".to_string());
}
let m_manifest = meta.get("doc_manifest_sha").and_then(serde_json::Value::as_str);
if m_manifest != Some(doc_manifest_sha) {
return Err("doc_manifest_sha mismatch".to_string());
}
let blob = fs::read(cache_path).map_err(|e| format!("read blob: {e}"))?;
parse_blob(&blob, identity.dimension).map(Some)
}
fn parse_blob(blob: &[u8], expected_dim: u32) -> Result<VecCache, String> {
let mut cur = blob;
let mut take = |n: usize| -> Result<&[u8], String> {
if cur.len() < n {
return Err("truncated blob".to_string());
}
let (head, tail) = cur.split_at(n);
cur = tail;
Ok(head)
};
let fmt = u32::from_le_bytes(take(4)?.try_into().unwrap());
if fmt != CACHE_FORMAT_VERSION {
return Err(format!("blob format_version mismatch (got {fmt})"));
}
let dim = u32::from_le_bytes(take(4)?.try_into().unwrap());
if dim != expected_dim {
return Err(format!("blob dim {dim} != expected {expected_dim}"));
}
let count = u32::from_le_bytes(take(4)?.try_into().unwrap()) as usize;
let dim = dim as usize;
let mut map = VecCache::with_capacity(count);
for _ in 0..count {
let key: [u8; 32] = take(32)?.try_into().unwrap();
let mut v = Vec::with_capacity(dim);
let vec_bytes = take(dim * 4)?;
for chunk in vec_bytes.chunks_exact(4) {
v.push(f32::from_le_bytes(chunk.try_into().unwrap()));
}
map.insert(key, v);
}
if !cur.is_empty() {
return Err("trailing bytes in blob".to_string());
}
Ok(map)
}
fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
let tmp = path
.with_extension(format!("{}.tmp", path.extension().and_then(|e| e.to_str()).unwrap_or("")));
{
let mut f = fs::File::create(&tmp).map_err(|e| format!("create {}: {e}", tmp.display()))?;
f.write_all(bytes).map_err(|e| format!("write {}: {e}", tmp.display()))?;
}
fs::rename(&tmp, path).map_err(|e| format!("rename -> {}: {e}", path.display()))?;
Ok(())
}
fn open_readonly(path: &Path) -> Connection {
Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.expect("open read-only sqlite")
}
struct SplitMix64 {
state: u64,
}
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn next_in(&mut self, bound: usize) -> usize {
(self.next_u64() % bound as u64) as usize
}
}
const LEAD_MAX_CHARS: usize = 140;
fn synth_query(doc: &Doc) -> Option<String> {
if let Some(title) = &doc.title {
let t = title.trim();
if t.len() >= 6 && !t.eq_ignore_ascii_case("untitled") && t != doc.body.trim() {
return Some(t.to_string());
}
}
let body = doc.body.trim();
if body.is_empty() {
return None;
}
let lead = lead_sentence(body, LEAD_MAX_CHARS);
if lead.trim().is_empty() || lead.trim() == body {
return None;
}
Some(lead)
}
fn lead_sentence(body: &str, max_chars: usize) -> String {
let cleaned: String = body
.lines()
.map(|l| l.trim_start_matches(['-', '*', '#', '>', ' ', '\t']))
.collect::<Vec<_>>()
.join(" ");
let cleaned = cleaned.trim();
let mut out = String::new();
for (i, ch) in cleaned.chars().enumerate() {
if i >= max_chars {
break;
}
out.push(ch);
if matches!(ch, '.' | '!' | '?') && out.trim().len() >= 12 {
break;
}
}
out.trim().to_string()
}