use std::path::{Path, PathBuf};
use anyhow::Result;
use rusqlite::Connection;
use super::{IndexStats, IndexSummary};
use crate::config::schema::Config;
use crate::embed::Backend;
use crate::engine::index_bridge::IndexBridge;
#[derive(Debug, Clone, Copy)]
pub enum ConfigSource<'a> {
Full(Option<&'a str>),
ProjectOnly,
#[cfg_attr(not(test), allow(dead_code))]
ProjectOnlyAt(&'a Path),
}
pub fn load_config(source: ConfigSource<'_>) -> Option<Config> {
match source {
ConfigSource::Full(path) => {
crate::config::loader::load_config(path, None, None, None, None, false).ok()
}
ConfigSource::ProjectOnly => std::env::current_dir()
.ok()
.and_then(|cwd| load_project_only(&cwd)),
ConfigSource::ProjectOnlyAt(start) => load_project_only(start),
}
}
pub fn load_project_only(start: &Path) -> Option<Config> {
let mut config = Config::default();
if let Some((_, cora)) = crate::config::loader::find_cora_file(start).ok()? {
cora.merge_into(&mut config).ok()?;
}
Some(config)
}
#[derive(Debug, Clone)]
#[cfg_attr(not(test), allow(dead_code))] pub struct Configured {
pub brain_mode: String,
pub backend: Backend,
pub skip_patterns: Option<Vec<String>>,
}
pub fn configure(config: Option<&Config>) -> Configured {
let brain_mode = config
.map(|c| c.brain.embedding.to_string())
.unwrap_or_else(|| "auto".to_string());
let backend = crate::embed::resolve_backend(&brain_mode);
super::vector::apply_config_store(config);
Configured {
brain_mode,
backend,
skip_patterns: super::skip_patterns_from_config(config),
}
}
pub fn configure_for_search(source: ConfigSource<'_>) -> Configured {
configure(load_config(source).as_ref())
}
pub struct IndexSession {
conn: Connection,
project_id: i64,
root: PathBuf,
skip_patterns: Option<Vec<String>>,
}
impl IndexSession {
pub fn open(source: ConfigSource<'_>) -> Result<Self> {
Self::open_at(&std::env::current_dir()?, source)
}
pub fn open_at(start: &Path, source: ConfigSource<'_>) -> Result<Self> {
let bridge = IndexBridge::open_or_create(start)?;
Self::from_bridge(bridge, load_config(source).as_ref())
}
pub fn from_bridge(bridge: IndexBridge, config: Option<&Config>) -> Result<Self> {
let (conn, project_id, root) = bridge.into_strict_parts()?;
let configured = configure(config);
Ok(Self {
conn,
project_id,
root,
skip_patterns: configured.skip_patterns,
})
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn skip_patterns(&self) -> Option<&[String]> {
self.skip_patterns.as_deref()
}
pub fn index(&self, verbose: bool) -> Result<IndexStats> {
super::index_project_with_skip(&self.conn, &self.root, verbose, self.skip_patterns())
}
pub fn index_matching(
&self,
verbose: bool,
include: &dyn Fn(&str) -> bool,
) -> Result<IndexStats> {
super::index_project_filtered(
&self.conn,
&self.root,
verbose,
self.skip_patterns(),
Some(include),
)
}
pub fn summary(&self) -> Result<IndexSummary> {
super::index_stats(&self.conn, self.project_id)
}
pub fn prune(&self) -> Result<usize> {
super::prune_deleted(&self.conn, self.project_id, &self.root)
}
pub fn rebuild(&mut self) -> Result<()> {
super::schema::delete_project(&self.conn, self.project_id)?;
self.project_id =
super::schema::get_or_create_project(&self.conn, &self.root.to_string_lossy())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn project(cora_yaml: &str) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
fs::write(root.join(".cora.yaml"), cora_yaml).unwrap();
fs::create_dir_all(root.join("src")).unwrap();
fs::write(root.join("src/a.rs"), "pub fn alpha() {}\n").unwrap();
fs::create_dir_all(root.join("gen")).unwrap();
fs::write(root.join("gen/b.rs"), "pub fn beta() {}\n").unwrap();
fs::create_dir_all(root.join("vendor")).unwrap();
fs::write(root.join("vendor/c.rs"), "pub fn gamma() {}\n").unwrap();
(dir, root)
}
const YAML: &str = "brain:\n embedding: hashing\nignore:\n files:\n - \"gen/**\"\nrules_engine:\n index_skip_files:\n - \"vendor/**\"\n";
fn memory_bridge(root: &Path) -> IndexBridge {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
crate::index::schema::run_migrations(&conn).unwrap();
IndexBridge::from_connection(conn, root).unwrap()
}
#[test]
fn project_only_config_merges_skip_sources_and_resolves_backend() {
let (_d, root) = project(YAML);
let config = load_project_only(&root).expect("config");
let configured = configure(Some(&config));
assert_eq!(configured.brain_mode, "hashing");
assert_eq!(configured.backend, Backend::Hashing);
let pats = configured.skip_patterns.unwrap();
assert!(pats.contains(&"gen/**".to_string()), "{pats:?}");
assert!(pats.contains(&"vendor/**".to_string()), "{pats:?}");
}
#[test]
fn search_setup_resolves_backend_from_project_config() {
let (_d, root) = project(YAML);
let configured = configure_for_search(ConfigSource::ProjectOnlyAt(&root));
assert_eq!(configured.brain_mode, "hashing");
assert_eq!(configured.backend, Backend::Hashing);
}
#[test]
fn explicit_config_path_is_honored_by_full_source() {
let (_d, root) = project("brain:\n embedding: hashing\n");
let other = root.join("other.yaml");
fs::write(&other, "ignore:\n files:\n - \"only-in-explicit/**\"\n").unwrap();
let config = load_config(ConfigSource::Full(other.to_str())).expect("config");
let pats = crate::index::skip_patterns_from_config(Some(&config)).unwrap();
assert!(
pats.contains(&"only-in-explicit/**".to_string()),
"{pats:?}"
);
}
#[test]
fn session_indexes_incrementally_and_excludes_skipped() {
let (_d, root) = project(YAML);
let config = load_project_only(&root);
let session = IndexSession::from_bridge(memory_bridge(&root), config.as_ref()).unwrap();
assert_eq!(session.root(), root.as_path());
let first = session.index(false).unwrap();
assert_eq!(first.files_indexed, 1, "only src/a.rs: {first:?}");
assert!(first.files_excluded >= 2, "{first:?}");
assert_eq!(session.summary().unwrap().total_files, 1);
let second = session.index(false).unwrap();
assert_eq!(second.files_indexed, 0, "{second:?}");
assert!(second.files_skipped >= 1, "{second:?}");
fs::write(
root.join("src/a.rs"),
"pub fn alpha() {}\npub fn alpha2() {}\n",
)
.unwrap();
let third = session.index(false).unwrap();
assert_eq!(third.files_indexed, 1, "{third:?}");
}
#[test]
fn rebuild_resets_project_and_prune_counts_deleted() {
let (_d, root) = project(YAML);
let config = load_project_only(&root);
let mut session = IndexSession::from_bridge(memory_bridge(&root), config.as_ref()).unwrap();
session.index(false).unwrap();
fs::remove_file(root.join("src/a.rs")).unwrap();
assert_eq!(session.prune().unwrap(), 1);
session.index(false).unwrap();
session.rebuild().unwrap();
assert_eq!(session.summary().unwrap().total_files, 0);
}
#[test]
fn scanners_and_indexing_share_one_skip_source() {
let (_d, root) = project(YAML);
let config = load_project_only(&root).unwrap();
let session = IndexSession::from_bridge(memory_bridge(&root), Some(&config)).unwrap();
let review_patterns = crate::engine::deterministic::skip_patterns(&config);
assert_eq!(Some(review_patterns.as_slice()), session.skip_patterns());
let matcher = crate::engine::path_match::PathMatcher::new(&review_patterns);
assert!(matcher.is_match("gen/b.rs"));
assert!(matcher.is_match("vendor/c.rs"));
}
}