Skip to content

Commit 96d0640

Browse files
committed
fix: remote semantic status reporting and Windows path tests (#98)
2 parents ef17cc3 + ed9e0fd commit 96d0640

9 files changed

Lines changed: 429 additions & 38 deletions

File tree

src/cli/commands/index_parallel.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
use std::path::{Path, PathBuf};
77
use std::sync::{Arc, Mutex};
88

9+
use crate::IndexPersistence;
910
use crate::config::Settings;
10-
use crate::indexing::facade::{build_embedding_backend, resolve_remote_model_name};
11+
use crate::indexing::facade::{
12+
build_embedding_backend, format_semantic_status, resolve_remote_model_name,
13+
};
1114
use crate::indexing::pipeline::{IncrementalStats, Phase2Stats, Pipeline, PipelineConfig};
1215
use crate::io::status_line::{ProgressBar, ProgressBarOptions, ProgressBarStyle};
1316
use crate::semantic::{EmbeddingBackend, SemanticSearchError, SimpleSemanticSearch};
@@ -121,6 +124,12 @@ pub fn run(args: IndexParallelArgs, settings: &Settings) {
121124
}
122125
}
123126

127+
let persistence = IndexPersistence::new(settings.index_path.clone());
128+
if let Err(e) = persistence.save_document_index_metadata(index.as_ref(), &paths_to_index) {
129+
tracing::error!(target: "pipeline", "Failed to persist index metadata: {e}");
130+
std::process::exit(1);
131+
}
132+
124133
tracing::info!(target: "pipeline", "Index saved to: {}", index_path.display());
125134
if semantic.is_some() {
126135
tracing::info!(target: "pipeline", "Embeddings saved to: {}", semantic_path.display());
@@ -157,6 +166,11 @@ fn create_semantic_search(
157166
};
158167

159168
let model = &settings.semantic_search.model;
169+
let effective_model = if is_remote {
170+
resolve_remote_model_name(&settings.semantic_search)
171+
} else {
172+
model.clone()
173+
};
160174

161175
// Load existing embeddings or create fresh instance.
162176
// After loading, verify dimensions match the backend so we don't silently
@@ -207,14 +221,19 @@ fn create_semantic_search(
207221
let new_result = if is_remote {
208222
Ok(SimpleSemanticSearch::new_empty(
209223
backend.dimensions(),
210-
&resolve_remote_model_name(&settings.semantic_search),
224+
&effective_model,
211225
))
212226
} else {
213227
SimpleSemanticSearch::from_model_name(model)
214228
};
215229
match new_result {
216230
Ok(s) => {
217-
tracing::debug!(target: "pipeline", "Created new semantic search with model: {model}");
231+
tracing::debug!(
232+
target: "pipeline",
233+
"Created new semantic search with model: {effective_model}"
234+
);
235+
let status = format_semantic_status(&settings.semantic_search);
236+
eprintln!("{status}");
218237
Some(Arc::new(Mutex::new(s)))
219238
}
220239
Err(e) => {

src/indexing/facade.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ pub struct IndexFacade {
9696
/// Set to true when load_semantic_search fails with DimensionMismatch so
9797
/// hot-reload and other callers do not retry on every reload cycle.
9898
semantic_incompatible: bool,
99+
100+
/// Persisted semantic metadata for status/reporting when semantic search
101+
/// is not loaded into memory (for example, lite facade loads).
102+
semantic_metadata_snapshot: Option<crate::semantic::SemanticMetadata>,
99103
}
100104

101105
impl IndexFacade {
@@ -126,6 +130,7 @@ impl IndexFacade {
126130
indexed_paths: HashSet::new(),
127131
index_base,
128132
semantic_incompatible: false,
133+
semantic_metadata_snapshot: None,
129134
})
130135
}
131136

@@ -151,6 +156,7 @@ impl IndexFacade {
151156
indexed_paths: HashSet::new(),
152157
index_base,
153158
semantic_incompatible: false,
159+
semantic_metadata_snapshot: None,
154160
}
155161
}
156162

@@ -201,6 +207,7 @@ impl IndexFacade {
201207
};
202208

203209
self.semantic_search = Some(Arc::new(Mutex::new(semantic)));
210+
self.semantic_metadata_snapshot = self.get_semantic_metadata();
204211
self.embedding_pool = Some(backend);
205212

206213
Ok(())
@@ -289,6 +296,7 @@ impl IndexFacade {
289296
}
290297

291298
self.semantic_search = Some(Arc::new(Mutex::new(semantic)));
299+
self.semantic_metadata_snapshot = self.get_semantic_metadata();
292300
return Ok(true);
293301
}
294302
Err(SemanticSearchError::DimensionMismatch {
@@ -324,6 +332,18 @@ impl IndexFacade {
324332
Ok(false)
325333
}
326334

335+
/// Load persisted semantic metadata without initializing the semantic backend.
336+
pub fn load_semantic_metadata_snapshot(&mut self, path: &Path) -> FacadeResult<bool> {
337+
if !path.join("metadata.json").exists() {
338+
self.semantic_metadata_snapshot = None;
339+
return Ok(false);
340+
}
341+
342+
let metadata = crate::semantic::SemanticMetadata::load(path)?;
343+
self.semantic_metadata_snapshot = Some(metadata);
344+
Ok(true)
345+
}
346+
327347
/// Ensure embedding backend is initialized for generating new embeddings.
328348
///
329349
/// Called lazily by methods that need to compute embeddings (reindexing, watcher).
@@ -343,6 +363,11 @@ impl IndexFacade {
343363
self.semantic_search
344364
.as_ref()
345365
.map(|s| s.lock().map(|sem| sem.embedding_count()).unwrap_or(0))
366+
.or_else(|| {
367+
self.semantic_metadata_snapshot
368+
.as_ref()
369+
.map(|m| m.embedding_count)
370+
})
346371
.unwrap_or(0)
347372
}
348373

@@ -351,6 +376,7 @@ impl IndexFacade {
351376
self.semantic_search
352377
.as_ref()
353378
.and_then(|s| s.lock().ok().and_then(|sem| sem.metadata().cloned()))
379+
.or_else(|| self.semantic_metadata_snapshot.clone())
354380
}
355381

356382
// =========================================================================
@@ -1224,6 +1250,20 @@ pub fn resolve_remote_model_name(cfg: &crate::config::SemanticSearchConfig) -> S
12241250
.unwrap_or_else(|| "text-embedding-ada-002".to_string())
12251251
}
12261252

1253+
/// Format a human-readable semantic search status line for CLI output.
1254+
pub fn format_semantic_status(cfg: &crate::config::SemanticSearchConfig) -> String {
1255+
let is_remote = std::env::var("CODANNA_EMBED_URL").is_ok() || cfg.remote_url.is_some();
1256+
let threshold = cfg.threshold;
1257+
1258+
if is_remote {
1259+
let model = resolve_remote_model_name(cfg);
1260+
format!("Semantic search enabled (backend: remote, model: {model}, threshold: {threshold})")
1261+
} else {
1262+
let model = &cfg.model;
1263+
format!("Semantic search enabled (model: {model}, threshold: {threshold})")
1264+
}
1265+
}
1266+
12271267
pub fn build_embedding_backend(
12281268
cfg: &crate::config::SemanticSearchConfig,
12291269
) -> FacadeResult<EmbeddingBackend> {

src/main.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use clap::Parser;
77
use codanna::cli::{Cli, Commands, RetrieveQuery};
8-
use codanna::indexing::facade::IndexFacade;
8+
use codanna::indexing::facade::{IndexFacade, format_semantic_status};
99
use codanna::project_resolver::{
1010
providers::{
1111
csharp::CSharpProvider, go::GoProvider, java::JavaProvider, javascript::JavaScriptProvider,
@@ -414,10 +414,8 @@ async fn main() {
414414
if let Err(e) = idx.enable_semantic_search() {
415415
eprintln!("Warning: Failed to enable semantic search: {e}");
416416
} else {
417-
eprintln!(
418-
"Semantic search enabled (model: {}, threshold: {})",
419-
config.semantic_search.model, config.semantic_search.threshold
420-
);
417+
let status = format_semantic_status(&config.semantic_search);
418+
eprintln!("{status}");
421419
}
422420
}
423421
}

src/parsing/java/behavior.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -963,16 +963,13 @@ mod tests {
963963
fs::write(&pom_path, pom_content).unwrap();
964964

965965
// Create settings and build provider cache
966-
let settings_content = format!(
967-
r#"
968-
[languages.java]
969-
enabled = true
970-
config_files = ["{}"]
971-
"#,
972-
pom_path.display()
973-
);
974-
975-
let settings: crate::config::Settings = toml::from_str(&settings_content).unwrap();
966+
let mut settings = crate::config::Settings::default();
967+
let java_settings = settings
968+
.languages
969+
.get_mut("java")
970+
.expect("java language config should exist");
971+
java_settings.enabled = true;
972+
java_settings.config_files = vec![pom_path.clone()];
976973

977974
// Save original directory to restore later
978975
let original_dir = std::env::current_dir().unwrap();

src/parsing/lua/behavior.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ impl LanguageBehavior for LuaBehavior {
250250
mod tests {
251251
use super::*;
252252
use crate::Visibility;
253-
use std::path::Path;
253+
use tempfile::TempDir;
254254

255255
#[test]
256256
fn test_module_separator() {
@@ -261,18 +261,19 @@ mod tests {
261261
#[test]
262262
fn test_module_path_from_file() {
263263
let behavior = LuaBehavior::new();
264-
let project_root = Path::new("/home/user/project");
264+
let temp_dir = TempDir::new().unwrap();
265+
let project_root = temp_dir.path();
265266
let extensions = &["lua"];
266267

267-
let file_path = Path::new("/home/user/project/lib/utils.lua");
268+
let file_path = project_root.join("lib/utils.lua");
268269
assert_eq!(
269-
behavior.module_path_from_file(file_path, project_root, extensions),
270+
behavior.module_path_from_file(&file_path, project_root, extensions),
270271
Some("lib.utils".to_string())
271272
);
272273

273-
let file_path = Path::new("/home/user/project/main.lua");
274+
let file_path = project_root.join("main.lua");
274275
assert_eq!(
275-
behavior.module_path_from_file(file_path, project_root, extensions),
276+
behavior.module_path_from_file(&file_path, project_root, extensions),
276277
Some("main".to_string())
277278
);
278279
}

src/parsing/paths.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,13 @@ pub fn strip_extension<'a>(path_str: &'a str, extensions: &[&str]) -> &'a str {
7474
#[cfg(test)]
7575
mod tests {
7676
use super::*;
77+
use tempfile::TempDir;
7778

7879
#[test]
7980
fn test_normalize_relative_path() {
8081
let file_path = Path::new("src/foo/bar.rs");
81-
let workspace_root = Path::new("/home/user/workspace");
82+
let temp_dir = TempDir::new().unwrap();
83+
let workspace_root = temp_dir.path();
8284

8385
let result = normalize_for_module_path(file_path, workspace_root);
8486

@@ -88,10 +90,11 @@ mod tests {
8890

8991
#[test]
9092
fn test_normalize_absolute_path() {
91-
let file_path = Path::new("/home/user/workspace/src/foo/bar.rs");
92-
let workspace_root = Path::new("/home/user/workspace");
93+
let temp_dir = TempDir::new().unwrap();
94+
let workspace_root = temp_dir.path();
95+
let file_path = workspace_root.join("src/foo/bar.rs");
9396

94-
let result = normalize_for_module_path(file_path, workspace_root);
97+
let result = normalize_for_module_path(&file_path, workspace_root);
9598

9699
assert_eq!(result, file_path);
97100
}

0 commit comments

Comments
 (0)