use std::path::PathBuf;
use std::sync::Arc;
use crate::kit::{
AsyncKit, AsyncReady, ExtractorRegistryModule, IndexerModule, KitError, ParserFactoryModule,
QueryModule, ResolverModule, StorageModule, TraceModule,
};
#[cfg(feature = "cache")]
use crate::kit::CacheModule;
#[cfg(feature = "daemon")]
use crate::kit::DaemonModule;
#[cfg(feature = "embed")]
use crate::kit::EmbedModule;
use crate::index::IndexConfig;
use crate::query::QueryConfig;
use crate::storage::StorageConfig;
use crate::trace::TraceConfig;
#[cfg(feature = "daemon")]
use crate::daemon::{DaemonConfig, DEFAULT_DEBOUNCE_MS};
#[cfg(feature = "embed")]
use crate::embed::EmbeddingConfig;
#[cfg(feature = "cache")]
use crate::cache::CacheConfig;
#[derive(Debug, Clone)]
pub struct KitBootstrapConfig {
pub db_path: PathBuf,
pub debounce_ms: u64,
#[cfg(feature = "embed")]
pub embedding_config: EmbeddingConfig,
}
impl KitBootstrapConfig {
#[must_use]
pub fn new(db_path: PathBuf) -> Self {
Self {
db_path,
debounce_ms: DEFAULT_DEBOUNCE_MS,
#[cfg(feature = "embed")]
embedding_config: EmbeddingConfig::from_env(),
}
}
#[must_use]
pub fn with_debounce_ms(mut self, debounce_ms: u64) -> Self {
self.debounce_ms = debounce_ms;
self
}
#[cfg(feature = "embed")]
#[must_use]
pub fn with_embedding_config(mut self, config: EmbeddingConfig) -> Self {
self.embedding_config = config;
self
}
}
#[cfg(not(feature = "daemon"))]
pub const DEFAULT_DEBOUNCE_MS: u64 = 2000;
pub async fn build_kit(config: &KitBootstrapConfig) -> Result<AsyncKit<AsyncReady>, KitError> {
let mut kit = AsyncKit::new();
kit.set_config(StorageConfig {
db_path: config.db_path.clone(),
});
kit.register::<StorageModule>()?;
kit.register::<ParserFactoryModule>()?;
kit.register::<ExtractorRegistryModule>()?;
kit.set_config(IndexConfig {
db_path: config.db_path.clone(),
});
kit.register::<IndexerModule>()?;
kit.register::<ResolverModule>()?;
kit.set_config(QueryConfig {
db_path: config.db_path.clone(),
});
kit.register::<QueryModule>()?;
kit.set_config(TraceConfig {
db_path: config.db_path.clone(),
..Default::default()
});
kit.register::<TraceModule>()?;
#[cfg(feature = "daemon")]
{
kit.set_config(DaemonConfig {
db_path: config.db_path.clone(),
debounce_ms: config.debounce_ms,
});
kit.register::<DaemonModule>()?;
}
#[cfg(feature = "embed")]
{
kit.set_config(config.embedding_config.clone());
kit.register::<EmbedModule>()?;
}
#[cfg(feature = "cache")]
{
kit.set_config(CacheConfig::default());
kit.register::<CacheModule>()?;
}
kit.build().await
}
pub trait KitExt {
fn require_storage(&self) -> Result<Arc<dyn crate::storage::capability::Storage>, KitError>;
fn require_parser(&self)
-> Result<Arc<dyn crate::parse::capability::ParserRegistry>, KitError>;
fn require_extractor(
&self,
) -> Result<Arc<dyn crate::parse::capability::ExtractorRegistry>, KitError>;
fn require_indexer(&self) -> Result<Arc<dyn crate::index::capability::Indexer>, KitError>;
fn require_resolver(&self) -> Result<Arc<dyn crate::resolve::capability::Resolver>, KitError>;
fn require_query(&self) -> Result<Arc<dyn crate::query::capability::QueryEngine>, KitError>;
fn require_trace(&self) -> Result<Arc<dyn crate::trace::capability::TraceEngine>, KitError>;
#[cfg(feature = "daemon")]
fn require_daemon(&self) -> Result<Arc<dyn crate::daemon::capability::DaemonRunner>, KitError>;
#[cfg(feature = "embed")]
fn require_embed(&self) -> Result<Arc<dyn crate::embed::client::EmbedClient>, KitError>;
#[cfg(feature = "cache")]
fn require_cache(&self) -> Result<Arc<dyn crate::cache::CacheStore>, KitError>;
}
impl KitExt for AsyncKit<AsyncReady> {
fn require_storage(&self) -> Result<Arc<dyn crate::storage::capability::Storage>, KitError> {
self.require::<StorageModule>()
}
fn require_parser(
&self,
) -> Result<Arc<dyn crate::parse::capability::ParserRegistry>, KitError> {
self.require::<ParserFactoryModule>()
}
fn require_extractor(
&self,
) -> Result<Arc<dyn crate::parse::capability::ExtractorRegistry>, KitError> {
self.require::<ExtractorRegistryModule>()
}
fn require_indexer(&self) -> Result<Arc<dyn crate::index::capability::Indexer>, KitError> {
self.require::<IndexerModule>()
}
fn require_resolver(&self) -> Result<Arc<dyn crate::resolve::capability::Resolver>, KitError> {
self.require::<ResolverModule>()
}
fn require_query(&self) -> Result<Arc<dyn crate::query::capability::QueryEngine>, KitError> {
self.require::<QueryModule>()
}
fn require_trace(&self) -> Result<Arc<dyn crate::trace::capability::TraceEngine>, KitError> {
self.require::<TraceModule>()
}
#[cfg(feature = "daemon")]
fn require_daemon(&self) -> Result<Arc<dyn crate::daemon::capability::DaemonRunner>, KitError> {
self.require::<DaemonModule>()
}
#[cfg(feature = "embed")]
fn require_embed(&self) -> Result<Arc<dyn crate::embed::client::EmbedClient>, KitError> {
self.require::<EmbedModule>()
}
#[cfg(feature = "cache")]
fn require_cache(&self) -> Result<Arc<dyn crate::cache::CacheStore>, KitError> {
self.require::<CacheModule>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn build_kit_in_memory_registers_all_core_capabilities() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
assert!(kit.contains::<StorageModule>(), "StorageModule missing");
assert!(
kit.contains::<ParserFactoryModule>(),
"ParserFactoryModule missing"
);
assert!(
kit.contains::<ExtractorRegistryModule>(),
"ExtractorRegistryModule missing"
);
assert!(kit.contains::<IndexerModule>(), "IndexerModule missing");
assert!(kit.contains::<ResolverModule>(), "ResolverModule missing");
assert!(kit.contains::<QueryModule>(), "QueryModule missing");
assert!(kit.contains::<TraceModule>(), "TraceModule missing");
}
#[tokio::test]
async fn build_kit_require_returns_registered_arc() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
let storage = kit.require_storage().expect("require_storage");
storage.init_schema().expect("init_schema");
let _query = kit.require_query().expect("require_query");
let _trace = kit.require_trace().expect("require_trace");
}
#[tokio::test]
async fn build_kit_invalid_db_path_returns_build_failed_error() {
let config = KitBootstrapConfig::new(PathBuf::from("/nonexistent/dir/xyz/db.lbug"));
let result = build_kit(&config).await;
assert!(
result.is_err(),
"expected build_kit to fail on invalid db_path"
);
let err = result.err().unwrap();
let msg = err.to_string();
assert!(
msg.contains("storage") || msg.contains("build"),
"error should mention storage/build, got: {msg}"
);
}
#[test]
fn bootstrap_config_new_defaults_debounce_ms() {
let config = KitBootstrapConfig::new(PathBuf::from("/tmp/db.lbug"));
assert_eq!(config.debounce_ms, 2000, "BR-DAEMON-001 default debounce");
assert_eq!(config.db_path, PathBuf::from("/tmp/db.lbug"));
}
#[test]
fn bootstrap_config_with_debounce_ms_overrides_default() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:")).with_debounce_ms(500);
assert_eq!(config.debounce_ms, 500);
}
#[cfg(feature = "daemon")]
#[tokio::test]
async fn build_kit_registers_daemon_when_feature_on() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
assert!(
kit.contains::<DaemonModule>(),
"DaemonModule missing with daemon feature"
);
let _daemon = kit.require_daemon().expect("require_daemon");
}
#[cfg(not(feature = "daemon"))]
#[tokio::test]
async fn build_kit_omits_daemon_when_feature_off() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
assert!(kit.contains::<StorageModule>());
assert!(kit.contains::<ParserFactoryModule>());
assert!(kit.contains::<ExtractorRegistryModule>());
assert!(kit.contains::<IndexerModule>());
assert!(kit.contains::<ResolverModule>());
assert!(kit.contains::<QueryModule>());
assert!(kit.contains::<TraceModule>());
}
#[cfg(feature = "embed")]
#[tokio::test]
async fn build_kit_registers_embed_when_feature_on() {
std::env::remove_var(crate::embed::API_KEY_ENV);
std::env::remove_var(crate::embed::OPENAI_API_KEY_ENV);
std::env::remove_var(crate::embed::EMBED_ENDPOINT_ENV);
std::env::remove_var(crate::embed::EMBED_MODEL_PATH_ENV);
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
assert!(
kit.contains::<EmbedModule>(),
"EmbedModule missing with embed feature"
);
let embed = kit.require_embed().expect("require_embed");
let result = embed.embed(&["hello"]);
assert!(
matches!(result, Err(crate::embed::EmbedError::Unavailable(ref msg)) if msg.contains("not found")),
"expected Unavailable (model not found) in local mode, got {result:?}"
);
}
#[cfg(feature = "embed")]
#[tokio::test]
async fn build_kit_embed_remote_without_key_returns_missing_api_key() {
std::env::remove_var(crate::embed::API_KEY_ENV);
std::env::remove_var(crate::embed::OPENAI_API_KEY_ENV);
std::env::set_var(
crate::embed::EMBED_ENDPOINT_ENV,
"https://api.openai.com/v1",
);
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
let embed = kit.require_embed().expect("require_embed");
let result = embed.embed(&["hello"]);
assert!(
matches!(result, Err(crate::embed::EmbedError::MissingApiKey)),
"expected MissingApiKey in remote mode, got {result:?}"
);
std::env::remove_var(crate::embed::EMBED_ENDPOINT_ENV);
}
#[cfg(feature = "embed")]
#[test]
fn bootstrap_config_with_embedding_config_overrides_from_env() {
let custom = EmbeddingConfig {
endpoint: Some("https://custom.example.com/v1".to_string()),
model: "custom-model".to_string(),
api_key: Some("custom-key".to_string()),
..EmbeddingConfig::default()
};
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"))
.with_embedding_config(custom.clone());
assert_eq!(config.embedding_config.endpoint, custom.endpoint);
assert_eq!(config.embedding_config.model, custom.model);
assert_eq!(config.embedding_config.api_key, custom.api_key);
}
#[cfg(not(feature = "embed"))]
#[tokio::test]
async fn build_kit_omits_embed_when_feature_off() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
assert!(kit.contains::<StorageModule>());
assert!(kit.contains::<ParserFactoryModule>());
assert!(kit.contains::<ExtractorRegistryModule>());
assert!(kit.contains::<IndexerModule>());
assert!(kit.contains::<ResolverModule>());
assert!(kit.contains::<QueryModule>());
assert!(kit.contains::<TraceModule>());
}
#[tokio::test]
async fn convenience_helpers_match_direct_require() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
let via_helper = kit.require_storage().expect("require_storage");
let via_direct: Arc<dyn crate::storage::capability::Storage> = kit
.require::<StorageModule>()
.expect("require::<StorageModule>");
assert!(
Arc::ptr_eq(&via_helper, &via_direct),
"convenience helper must return the same Arc"
);
}
#[cfg(any(
feature = "lang-c",
feature = "lang-rust",
feature = "lang-fortran",
feature = "lang-python",
feature = "lang-typescript",
feature = "lang-go",
feature = "lang-java",
feature = "lang-cpp",
feature = "lang-javascript",
feature = "lang-ruby",
feature = "lang-haskell",
feature = "lang-ocaml",
feature = "lang-scala",
feature = "lang-php",
feature = "lang-csharp",
feature = "lang-bash",
feature = "lang-html",
feature = "lang-css",
feature = "lang-json",
feature = "lang-regex",
feature = "lang-verilog",
))]
#[tokio::test]
async fn convenience_helpers_return_registered_capabilities() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
let parser = kit.require_parser().expect("require_parser");
let langs = parser.supported_languages();
assert!(
!langs.is_empty(),
"parser should support at least one language"
);
let extractor = kit.require_extractor().expect("require_extractor");
let ext_langs = extractor.supported_languages();
assert!(
!ext_langs.is_empty(),
"extractor should support at least one language"
);
let _indexer = kit.require_indexer().expect("require_indexer");
let _resolver = kit.require_resolver().expect("require_resolver");
}
#[tokio::test]
async fn convenience_helpers_all_match_direct_require() {
let config = KitBootstrapConfig::new(PathBuf::from(":memory:"));
let kit = build_kit(&config).await.expect("build_kit");
let parser_helper = kit.require_parser().expect("require_parser");
let parser_direct: Arc<dyn crate::parse::capability::ParserRegistry> = kit
.require::<ParserFactoryModule>()
.expect("require::<ParserFactoryModule>");
assert!(Arc::ptr_eq(&parser_helper, &parser_direct));
let extractor_helper = kit.require_extractor().expect("require_extractor");
let extractor_direct: Arc<dyn crate::parse::capability::ExtractorRegistry> = kit
.require::<ExtractorRegistryModule>()
.expect("require::<ExtractorRegistryModule>");
assert!(Arc::ptr_eq(&extractor_helper, &extractor_direct));
let indexer_helper = kit.require_indexer().expect("require_indexer");
let indexer_direct: Arc<dyn crate::index::capability::Indexer> = kit
.require::<IndexerModule>()
.expect("require::<IndexerModule>");
assert!(Arc::ptr_eq(&indexer_helper, &indexer_direct));
let resolver_helper = kit.require_resolver().expect("require_resolver");
let resolver_direct: Arc<dyn crate::resolve::capability::Resolver> = kit
.require::<ResolverModule>()
.expect("require::<ResolverModule>");
assert!(Arc::ptr_eq(&resolver_helper, &resolver_direct));
}
}