use crate::chunking::DEFAULT_CHUNK_LINES;
use crate::error::{ClaudixError, RecoveryHint, Result};
use super::{Config, EmbeddingProvider, validate_project_relative_path};
pub fn validate(config: &Config) -> Result<()> {
validate_project_relative_path(&config.paths.index_dir, "paths.index_dir")?;
validate_project_relative_path(&config.paths.log_dir, "paths.log_dir")?;
let allow_stub_embedding_model = allow_stub_embedding_model(config);
if matches!(config.embedding.provider, EmbeddingProvider::Http)
&& config.embedding.endpoint.trim().is_empty()
{
return Err(ClaudixError::ConfigInvalid {
message: "embedding.endpoint is required when embedding.provider = \"http\"".into(),
recovery: RecoveryHint("Set [embedding].endpoint or switch provider to bundled"),
});
}
if matches!(config.embedding.provider, EmbeddingProvider::Bundled)
&& !allow_stub_embedding_model
&& config.embedding.model != crate::embedding::bundled::BUNDLED_MODEL_ID
{
return Err(ClaudixError::ConfigInvalid {
message: format!(
"embedding.model must be {} when embedding.provider = \"bundled\"",
crate::embedding::bundled::BUNDLED_MODEL_ID
),
recovery: RecoveryHint(
"Set [embedding].model = \"bge-small-en-v1.5\" for the bundled provider",
),
});
}
if matches!(config.embedding.provider, EmbeddingProvider::Bundled)
&& !allow_stub_embedding_model
&& config.embedding.dimensions != crate::embedding::bundled::BUNDLED_DIMENSIONS.0
{
return Err(ClaudixError::ConfigInvalid {
message: format!(
"embedding.dimensions must be {} when embedding.provider = \"bundled\"",
crate::embedding::bundled::BUNDLED_DIMENSIONS.0
),
recovery: RecoveryHint("Set [embedding].dimensions = 384 for the bundled provider"),
});
}
if config.embedding.dimensions == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "embedding.dimensions must be > 0".into(),
recovery: RecoveryHint("Set [embedding].dimensions to a positive integer"),
});
}
if config.embedding.batch_size == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "embedding.batch_size must be > 0".into(),
recovery: RecoveryHint("Set [embedding].batch_size to a positive integer"),
});
}
if config.embedding.timeout_ms == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "embedding.timeout_ms must be > 0".into(),
recovery: RecoveryHint("Set [embedding].timeout_ms to a positive integer"),
});
}
if config.indexing.max_file_size_kb == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "indexing.max_file_size_kb must be > 0".into(),
recovery: RecoveryHint("Set [indexing].max_file_size_kb to a positive integer"),
});
}
if config.indexing.chunk_overlap_lines >= DEFAULT_CHUNK_LINES {
return Err(ClaudixError::ConfigInvalid {
message: format!(
"indexing.chunk_overlap_lines must be less than {DEFAULT_CHUNK_LINES}"
),
recovery: RecoveryHint("Set [indexing].chunk_overlap_lines between 0 and 59"),
});
}
if config.indexing.reindex_after_hours == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "indexing.reindex_after_hours must be > 0".into(),
recovery: RecoveryHint("Set [indexing].reindex_after_hours to a positive integer"),
});
}
if config.hooks.related_top_k == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "hooks.related_top_k must be > 0".into(),
recovery: RecoveryHint("Set [hooks].related_top_k to a positive integer"),
});
}
if !(0.0..=1.0).contains(&config.hooks.related_min_similarity) {
return Err(ClaudixError::ConfigInvalid {
message: "hooks.related_min_similarity must be between 0 and 1".into(),
recovery: RecoveryHint("Set [hooks].related_min_similarity to a value in [0, 1]"),
});
}
if config.search.top_k == 0 {
return Err(ClaudixError::ConfigInvalid {
message: "search.top_k must be > 0".into(),
recovery: RecoveryHint("Set [search].top_k to a positive integer"),
});
}
if !(0.0..=1.0).contains(&config.search.similarity_threshold) {
return Err(ClaudixError::ConfigInvalid {
message: "search.similarity_threshold must be between 0 and 1".into(),
recovery: RecoveryHint("Set [search].similarity_threshold to a value in [0, 1]"),
});
}
if !(0.0..=1.0).contains(&config.search.min_score) {
return Err(ClaudixError::ConfigInvalid {
message: "search.min_score must be between 0 and 1".into(),
recovery: RecoveryHint("Set [search].min_score to a value in [0, 1]"),
});
}
if config
.search
.cross_repos
.iter()
.any(|repo| repo.trim().is_empty())
{
return Err(ClaudixError::ConfigInvalid {
message: "search.cross_repos entries must be non-empty paths".into(),
recovery: RecoveryHint("Remove empty strings from [search].cross_repos"),
});
}
if !config.search.identifier_boost.is_finite() || config.search.identifier_boost <= 0.0 {
return Err(ClaudixError::ConfigInvalid {
message: "search.identifier_boost must be a finite positive number".into(),
recovery: RecoveryHint("Set [search].identifier_boost to a finite positive number"),
});
}
let weights = &config.search.hybrid_weights;
if !weights.dense.is_finite() || !weights.bm25.is_finite() || !weights.rrf.is_finite() {
return Err(ClaudixError::ConfigInvalid {
message: "search.hybrid_weights components must be finite numbers".into(),
recovery: RecoveryHint("Set all [search].hybrid_weights values to finite numbers"),
});
}
if weights.dense < 0.0 || weights.bm25 < 0.0 || weights.rrf < 0.0 {
return Err(ClaudixError::ConfigInvalid {
message: "search.hybrid_weights components must be >= 0".into(),
recovery: RecoveryHint(
"Set all [search].hybrid_weights values to non-negative numbers",
),
});
}
if weights.dense == 0.0 && weights.bm25 == 0.0 && weights.rrf == 0.0 {
return Err(ClaudixError::ConfigInvalid {
message: "search.hybrid_weights must not all be zero".into(),
recovery: RecoveryHint(
"Set at least one of [search].hybrid_weights.dense, .bm25, .rrf to a positive value",
),
});
}
Ok(())
}
#[cfg(any(test, feature = "test-stub"))]
fn allow_stub_embedding_model(config: &Config) -> bool {
config.embedding.model.starts_with("stub")
}
#[cfg(not(any(test, feature = "test-stub")))]
fn allow_stub_embedding_model(_config: &Config) -> bool {
false
}