From f5bf774f04520e9e8a572445fdc553ce5199e9cd Mon Sep 17 00:00:00 2001 From: suknna Date: Thu, 4 Jun 2026 10:13:35 +0800 Subject: [PATCH] feat(search): expose semantic index size diagnostics --- crates/aft/src/cli/warmup.rs | 3 + crates/aft/src/commands/configure.rs | 83 ++++++++- crates/aft/src/commands/semantic_search.rs | 2 + crates/aft/src/commands/status.rs | 59 ++++++- crates/aft/src/context.rs | 19 ++ crates/aft/src/main.rs | 4 + crates/aft/src/semantic_index.rs | 165 +++++++++++++++++- .../integration/aft_search_contract_test.rs | 2 + 8 files changed, 325 insertions(+), 12 deletions(-) diff --git a/crates/aft/src/cli/warmup.rs b/crates/aft/src/cli/warmup.rs index 1802cbcf..8b448b87 100644 --- a/crates/aft/src/cli/warmup.rs +++ b/crates/aft/src/cli/warmup.rs @@ -384,6 +384,7 @@ fn semantic_index_state(ctx: &AppContext) -> SubsystemState { files, entries_done, entries_total, + .. } => { let mut detail = stage; if let Some(files) = files { @@ -453,12 +454,14 @@ fn drain_semantic_index_events(ctx: &AppContext) { files, entries_done, entries_total, + stats, } => { *ctx.semantic_index_status().borrow_mut() = SemanticIndexStatus::Building { stage, files, entries_done, entries_total, + stats, }; } SemanticIndexEvent::Ready(index) => { diff --git a/crates/aft/src/commands/configure.rs b/crates/aft/src/commands/configure.rs index e1fe8388..e9995ef6 100644 --- a/crates/aft/src/commands/configure.rs +++ b/crates/aft/src/commands/configure.rs @@ -15,8 +15,8 @@ use std::collections::{HashMap, HashSet}; use crate::callgraph::CallGraph; use crate::config::{SemanticBackend, SemanticBackendConfig, UserServerDef}; use crate::context::{ - AppContext, SemanticIndexEvent, SemanticIndexStatus, SemanticRefreshEvent, - SemanticRefreshRequest, SemanticRefreshWorkerSlot, + AppContext, SemanticIndexEvent, SemanticIndexStatus, SemanticProgressStats, + SemanticRefreshEvent, SemanticRefreshRequest, SemanticRefreshWorkerSlot, }; use crate::harness::Harness; use crate::log_ctx; @@ -27,7 +27,9 @@ use crate::search_index::{ build_path_filters, current_git_head, project_cache_key, resolve_cache_dir, walk_project_files_bounded_matching, CacheLock, SearchIndex, }; -use crate::semantic_index::{is_semantic_indexed_extension, SemanticIndex, SemanticIndexLock}; +use crate::semantic_index::{ + is_semantic_indexed_extension, SemanticIndex, SemanticIndexLock, SemanticMemoryEstimate, +}; use crate::{slog_info, slog_warn}; static WATCHER_GENERATION: AtomicU64 = AtomicU64::new(0); @@ -38,6 +40,39 @@ const SEMANTIC_REFRESH_MAX_BATCH_PATHS: usize = 50; const MAX_SEMANTIC_TIMEOUT_MS: u64 = 120_000; const MAX_SEMANTIC_BATCH_SIZE: usize = 1_024; +fn semantic_progress_stats_from_estimate( + estimate: SemanticMemoryEstimate, +) -> SemanticProgressStats { + SemanticProgressStats { + indexed_files: Some(estimate.indexed_files), + entries: Some(estimate.entries), + vector_bytes: Some(estimate.vector_bytes), + snippet_bytes: Some(estimate.snippet_bytes), + embed_text_bytes: Some(estimate.embed_text_bytes), + metadata_bytes: Some(estimate.metadata_bytes), + estimated_payload_bytes: Some(estimate.estimated_payload_bytes), + cache_bytes: None, + clone_estimated_bytes: None, + } +} + +fn semantic_clone_stats_from_estimate(estimate: SemanticMemoryEstimate) -> SemanticProgressStats { + SemanticProgressStats { + clone_estimated_bytes: Some(estimate.estimated_payload_bytes), + ..semantic_progress_stats_from_estimate(estimate) + } +} + +fn semantic_progress_stats_with_cache( + index: &SemanticIndex, + cache_bytes: u64, +) -> SemanticProgressStats { + SemanticProgressStats { + cache_bytes: Some(cache_bytes), + ..semantic_progress_stats_from_estimate(index.memory_estimate()) + } +} + fn resolve_home_dir() -> Option { let raw = std::env::var_os("HOME") .or_else(|| std::env::var_os("USERPROFILE")) @@ -196,9 +231,18 @@ fn spawn_semantic_refresh_worker( summary.total_processed, ); } + let clone_estimate = index.memory_estimate(); + let clone_started = Instant::now(); + let refreshed_index = index.clone(); + slog_info!( + "semantic corpus refresh clone: {} entries, {} estimated payload bytes, {} ms", + clone_estimate.entries, + clone_estimate.estimated_payload_bytes, + clone_started.elapsed().as_millis(), + ); if event_tx .send(SemanticRefreshEvent::CorpusCompleted { - index: index.clone(), + index: refreshed_index, changed: summary.changed, added: summary.added, deleted: summary.deleted, @@ -1934,6 +1978,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: None, entries_done: None, entries_total: None, + stats: None, }; let (tx, rx): ( crossbeam_channel::Sender, @@ -1974,6 +2019,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: None, entries_done: None, entries_total: None, + stats: None, }); let mut model = crate::semantic_index::EmbeddingModel::from_config(&semantic_config)?; @@ -2034,6 +2080,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: None, entries_done: None, entries_total: None, + stats: None, }); let mut progress = |done: usize, total: usize| { let _ = tx_progress.send(SemanticIndexEvent::Progress { @@ -2041,6 +2088,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: None, entries_done: Some(done), entries_total: Some(total), + stats: None, }); }; @@ -2079,6 +2127,10 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: None, entries_done: Some(cached.entry_count()), entries_total: Some(cached.entry_count()), + stats: Some(semantic_progress_stats_with_cache( + &cached, + cached.serialized_size_estimate(), + )), }); return Ok((cached, model)); } @@ -2104,6 +2156,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: Some(files.len()), entries_done: None, entries_total: None, + stats: None, }); files } @@ -2113,6 +2166,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: Some(observed), entries_done: None, entries_total: None, + stats: None, }); slog_warn!( "skipping semantic index: more than {} files exceeds limit of {}. \ @@ -2134,6 +2188,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: Some(files.len()), entries_done: None, entries_total: None, + stats: None, }); let mut progress = |done: usize, total: usize| { let _ = tx_progress.send(SemanticIndexEvent::Progress { @@ -2141,6 +2196,7 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: Some(files.len()), entries_done: Some(done), entries_total: Some(total), + stats: None, }); }; let index = SemanticIndex::build_with_progress( @@ -2162,6 +2218,10 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { files: Some(files.len()), entries_done: Some(index.len()), entries_total: Some(index.len()), + stats: Some(semantic_progress_stats_with_cache( + &index, + index.serialized_size_estimate(), + )), }); if !is_worktree_bridge_for_semantic { @@ -2176,7 +2236,22 @@ pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response { let event = match build_result { Ok(Ok((index, model))) => { + let clone_estimate = index.memory_estimate(); + let clone_started = Instant::now(); let worker_index = index.clone(); + slog_info!( + "semantic index clone for refresh worker: {} entries, {} estimated payload bytes, {} ms", + clone_estimate.entries, + clone_estimate.estimated_payload_bytes, + clone_started.elapsed().as_millis(), + ); + let _ = tx_progress.send(SemanticIndexEvent::Progress { + stage: "starting_refresh_worker".to_string(), + files: Some(clone_estimate.indexed_files), + entries_done: Some(clone_estimate.entries), + entries_total: Some(clone_estimate.entries), + stats: Some(semantic_clone_stats_from_estimate(clone_estimate)), + }); let worker_handle = spawn_semantic_refresh_worker( root_clone.clone(), worker_index, diff --git a/crates/aft/src/commands/semantic_search.rs b/crates/aft/src/commands/semantic_search.rs index f6a00685..fcb9eb57 100644 --- a/crates/aft/src/commands/semantic_search.rs +++ b/crates/aft/src/commands/semantic_search.rs @@ -364,6 +364,7 @@ fn handle_semantic_or_hybrid_search( files, entries_done, entries_total, + .. } => { let mut detail = format!("Semantic index is still building (stage: {}).", stage); if let Some(files) = files { @@ -1851,6 +1852,7 @@ mod tests { files: Some(1), entries_done: Some(0), entries_total: Some(1), + stats: None, }; let response = response_value(handle_semantic_search( diff --git a/crates/aft/src/commands/status.rs b/crates/aft/src/commands/status.rs index 7b8e8395..0c6f7cc5 100644 --- a/crates/aft/src/commands/status.rs +++ b/crates/aft/src/commands/status.rs @@ -1,7 +1,7 @@ //! AFT status command — returns the current state of indexes, features, and configuration. use crate::context::AppContext; -use crate::context::SemanticIndexStatus; +use crate::context::{SemanticIndexStatus, SemanticProgressStats}; use crate::db::compression_events::CompressionAggregate; use crate::protocol::{RawRequest, Response, StatusPayload, DEFAULT_SESSION_ID}; @@ -19,6 +19,23 @@ pub struct CompressionAggregateSerde { pub savings_tokens: u64, } +fn semantic_progress_stats_json(stats: Option) -> serde_json::Value { + let Some(stats) = stats else { + return serde_json::Value::Null; + }; + serde_json::json!({ + "indexed_files": stats.indexed_files, + "entries": stats.entries, + "vector_bytes": stats.vector_bytes, + "snippet_bytes": stats.snippet_bytes, + "embed_text_bytes": stats.embed_text_bytes, + "metadata_bytes": stats.metadata_bytes, + "estimated_payload_bytes": stats.estimated_payload_bytes, + "cache_bytes": stats.cache_bytes, + "clone_estimated_bytes": stats.clone_estimated_bytes, + }) +} + impl From for CompressionAggregateSerde { fn from(agg: CompressionAggregate) -> Self { Self { @@ -104,6 +121,7 @@ impl AppContext { files, entries_done, entries_total, + stats, } => serde_json::json!({ "status": "loading", "state": "loading", @@ -112,6 +130,7 @@ impl AppContext { "files": files, "entries_done": entries_done, "entries_total": entries_total, + "stats": semantic_progress_stats_json(stats), "backend": config.semantic_backend_label(), "model": config.semantic.model.as_str(), }), @@ -296,7 +315,7 @@ fn dir_size_recursive(path: &std::path::Path) -> u64 { mod tests { use super::handle_status; use crate::config::Config; - use crate::context::AppContext; + use crate::context::{AppContext, SemanticIndexStatus, SemanticProgressStats}; use crate::parser::TreeSitterProvider; use crate::protocol::RawRequest; use serde_json::json; @@ -330,4 +349,40 @@ mod tests { let response = handle_status(&request(), &ctx); assert_eq!(response.data["cache_role"], "worktree"); } + + #[test] + fn status_exposes_semantic_progress_stats() { + let ctx = AppContext::new(Box::new(TreeSitterProvider::new()), Config::default()); + ctx.config_mut().semantic_search = true; + *ctx.semantic_index_status().borrow_mut() = SemanticIndexStatus::Building { + stage: "persisting_index".to_string(), + files: Some(3), + entries_done: Some(5), + entries_total: Some(5), + stats: Some(SemanticProgressStats { + indexed_files: Some(3), + entries: Some(5), + vector_bytes: Some(7680), + snippet_bytes: Some(1200), + embed_text_bytes: Some(3400), + metadata_bytes: Some(260), + estimated_payload_bytes: Some(12540), + cache_bytes: Some(15000), + clone_estimated_bytes: None, + }), + }; + + let response = handle_status(&request(), &ctx); + let stats = &response.data["semantic_index"]["stats"]; + assert_eq!(response.data["semantic_index"]["stage"], "persisting_index"); + assert_eq!(stats["indexed_files"], 3); + assert_eq!(stats["entries"], 5); + assert_eq!(stats["vector_bytes"], 7680); + assert_eq!(stats["snippet_bytes"], 1200); + assert_eq!(stats["embed_text_bytes"], 3400); + assert_eq!(stats["metadata_bytes"], 260); + assert_eq!(stats["estimated_payload_bytes"], 12540); + assert_eq!(stats["cache_bytes"], 15000); + assert!(stats["clone_estimated_bytes"].is_null()); + } } diff --git a/crates/aft/src/context.rs b/crates/aft/src/context.rs index cfc3a5d9..98673e26 100644 --- a/crates/aft/src/context.rs +++ b/crates/aft/src/context.rs @@ -168,6 +168,7 @@ pub enum SemanticIndexStatus { files: Option, entries_done: Option, entries_total: Option, + stats: Option, }, Ready { /// Files currently being re-embedded after recent edits. The index is @@ -267,11 +268,29 @@ pub enum SemanticIndexEvent { files: Option, entries_done: Option, entries_total: Option, + stats: Option, }, Ready(SemanticIndex), Failed(String), } +/// Optional diagnostic counters attached to semantic-index progress updates. +/// +/// These are intentionally estimates: they expose payload/cache/clone pressure +/// without requiring a platform-specific heap profiler in the Rust worker. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SemanticProgressStats { + pub indexed_files: Option, + pub entries: Option, + pub vector_bytes: Option, + pub snippet_bytes: Option, + pub embed_text_bytes: Option, + pub metadata_bytes: Option, + pub estimated_payload_bytes: Option, + pub cache_bytes: Option, + pub clone_estimated_bytes: Option, +} + #[derive(Debug, Clone)] pub enum SemanticRefreshRequest { Files { paths: Vec }, diff --git a/crates/aft/src/main.rs b/crates/aft/src/main.rs index 3ed3727a..2fa9c116 100644 --- a/crates/aft/src/main.rs +++ b/crates/aft/src/main.rs @@ -1034,6 +1034,7 @@ fn refresh_corpus_after_ignore_change(ctx: &AppContext) { files: Some(file_count), entries_done: None, entries_total: None, + stats: None, }; match sender.send(SemanticRefreshRequest::Corpus { current_files }) { Ok(()) => { @@ -1332,12 +1333,14 @@ fn drain_semantic_index_events(ctx: &AppContext) { files, entries_done, entries_total, + stats, } => { *ctx.semantic_index_status().borrow_mut() = SemanticIndexStatus::Building { stage, files, entries_done, entries_total, + stats, }; // Push progress to the sidebar. Without this, a long rebuild // (e.g. a slow local embedding backend re-indexing after a prior @@ -1388,6 +1391,7 @@ fn drain_semantic_index_events(ctx: &AppContext) { files: Some(file_count), entries_done: None, entries_total: None, + stats: None, }; let sent = ctx.semantic_refresh_sender().is_some_and(|sender| { sender diff --git a/crates/aft/src/semantic_index.rs b/crates/aft/src/semantic_index.rs index 1ca5611b..6464db62 100644 --- a/crates/aft/src/semantic_index.rs +++ b/crates/aft/src/semantic_index.rs @@ -1065,6 +1065,22 @@ pub struct EmbeddingEntry { vector: Vec, } +/// Approximate payload footprint for the in-memory semantic index. +/// +/// The estimate covers strings and embedding vectors that scale with project +/// size. It intentionally does not try to model allocator or `HashMap` bucket +/// overhead, which is platform-specific and better measured by profilers. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SemanticMemoryEstimate { + pub indexed_files: usize, + pub entries: usize, + pub vector_bytes: u64, + pub snippet_bytes: u64, + pub embed_text_bytes: u64, + pub metadata_bytes: u64, + pub estimated_payload_bytes: u64, +} + /// The semantic index — stores embeddings for all symbols in a project #[derive(Debug, Clone)] pub struct SemanticIndex { @@ -1152,6 +1168,72 @@ impl SemanticIndex { self.file_mtimes.len() } + /// Estimate project-size-dependent payload bytes held by this index. + pub fn memory_estimate(&self) -> SemanticMemoryEstimate { + let mut estimate = SemanticMemoryEstimate { + indexed_files: self.file_mtimes.len(), + entries: self.entries.len(), + ..SemanticMemoryEstimate::default() + }; + + for entry in &self.entries { + estimate.vector_bytes = estimate + .vector_bytes + .saturating_add((entry.vector.len() * std::mem::size_of::()) as u64); + estimate.snippet_bytes = estimate + .snippet_bytes + .saturating_add(entry.chunk.snippet.len() as u64); + estimate.embed_text_bytes = estimate + .embed_text_bytes + .saturating_add(entry.chunk.embed_text.len() as u64); + estimate.metadata_bytes = estimate + .metadata_bytes + .saturating_add(entry.chunk.name.len() as u64) + .saturating_add(entry.chunk.file.to_string_lossy().len() as u64); + } + + estimate.estimated_payload_bytes = estimate + .vector_bytes + .saturating_add(estimate.snippet_bytes) + .saturating_add(estimate.embed_text_bytes) + .saturating_add(estimate.metadata_bytes); + estimate + } + + /// Estimate the V6 cache payload size without allocating the serialized buffer. + pub fn serialized_size_estimate(&self) -> u64 { + let fingerprint_len = self + .fingerprint + .as_ref() + .map(|fingerprint| fingerprint.as_string().len() as u64) + .unwrap_or(0); + let mut total = 1 + 4 + 4 + 4 + fingerprint_len; + + total += 4; + for (path, _mtime) in &self.file_mtimes { + let Some(relative) = cache_relative_path(&self.project_root, path) else { + continue; + }; + let path_len = relative.to_string_lossy().len() as u64; + total += 4 + path_len + 8 + 4 + 8 + 32; + } + + for entry in &self.entries { + let Some(relative) = cache_relative_path(&self.project_root, &entry.chunk.file) else { + continue; + }; + total += 4 + relative.to_string_lossy().len() as u64; + total += 4 + entry.chunk.name.len() as u64; + total += 1; + total += 4 + 4 + 1; + total += 4 + entry.chunk.snippet.len() as u64; + total += 4 + entry.chunk.embed_text.len() as u64; + total += (entry.vector.len() * std::mem::size_of::()) as u64; + } + + total + } + /// Human-readable status label for the index. pub fn status_label(&self) -> &'static str { if self.entries.is_empty() { @@ -1207,11 +1289,19 @@ impl SemanticIndex { } } + let embed_text_bytes: u64 = chunks + .iter() + .map(|chunk| chunk.embed_text.len() as u64) + .sum(); + let snippet_bytes: u64 = chunks.iter().map(|chunk| chunk.snippet.len() as u64).sum(); + slog_info!( - "semantic collect: {} chunks from {} files in {} ms", + "semantic collect: {} chunks from {} files in {} ms (embed_text_bytes={}, snippet_bytes={})", chunks.len(), file_metadata.len(), - collect_started.elapsed().as_millis() + collect_started.elapsed().as_millis(), + embed_text_bytes, + snippet_bytes, ); (chunks, file_metadata) @@ -2041,7 +2131,10 @@ impl SemanticIndex { .unwrap_or(Duration::ZERO) .as_nanos() )); + let serialize_started = std::time::Instant::now(); let bytes = self.to_bytes(); + let serialize_ms = serialize_started.elapsed().as_millis(); + let write_started = std::time::Instant::now(); let write_result = (|| -> std::io::Result<()> { use std::io::Write; let mut file = fs::File::create(&tmp_path)?; @@ -2049,6 +2142,7 @@ impl SemanticIndex { file.sync_all()?; Ok(()) })(); + let write_ms = write_started.elapsed().as_millis(); if let Err(e) = write_result { slog_warn!("failed to write semantic index: {}", e); let _ = fs::remove_file(&tmp_path); @@ -2059,10 +2153,14 @@ impl SemanticIndex { let _ = fs::remove_file(&tmp_path); return; } + let estimate = self.memory_estimate(); slog_info!( - "semantic index persisted: {} entries, {:.1} KB", + "semantic index persisted: {} entries, {} cache bytes, {} payload bytes (serialize_ms={}, write_ms={})", self.entries.len(), - bytes.len() as f64 / 1024.0 + bytes.len(), + estimate.estimated_payload_bytes, + serialize_ms, + write_ms, ); } @@ -2091,7 +2189,9 @@ impl SemanticIndex { return None; } + let read_started = std::time::Instant::now(); let bytes = fs::read(&data_path).ok()?; + let read_ms = read_started.elapsed().as_millis(); let version = bytes[0]; if version != SEMANTIC_INDEX_VERSION_V6 { slog_info!( @@ -2104,8 +2204,10 @@ impl SemanticIndex { } return None; } + let decode_started = std::time::Instant::now(); match Self::from_bytes(&bytes, current_canonical_root) { Ok(index) => { + let decode_ms = decode_started.elapsed().as_millis(); if index.entries.is_empty() { slog_info!("cached semantic index is empty, will rebuild"); if !is_worktree_bridge { @@ -2126,9 +2228,14 @@ impl SemanticIndex { return None; } } + let estimate = index.memory_estimate(); slog_info!( - "loaded semantic index from disk: {} entries", - index.entries.len() + "loaded semantic index from disk: {} entries, {} cache bytes, {} payload bytes (read_ms={}, decode_ms={})", + index.entries.len(), + file_len, + estimate.estimated_payload_bytes, + read_ms, + decode_ms, ); Some(index) } @@ -3140,6 +3247,7 @@ mod tests { }); let bytes = index.to_bytes(); + assert_eq!(index.serialized_size_estimate(), bytes.len() as u64); let restored = SemanticIndex::from_bytes(&bytes, &project_root).unwrap(); assert_eq!(restored.entries.len(), 1); @@ -3150,6 +3258,51 @@ mod tests { assert_eq!(restored.model_label(), Some("all-MiniLM-L6-v2")); } + #[test] + fn memory_estimate_counts_scaling_payload_bytes() { + let project_root = test_project_root(); + let file = project_root.join("src/main.rs"); + let mut index = SemanticIndex::new(project_root.clone(), 4); + index.entries.push(EmbeddingEntry { + chunk: SemanticChunk { + file: file.clone(), + name: "handle_request".to_string(), + kind: SymbolKind::Function, + start_line: 10, + end_line: 25, + exported: true, + embed_text: "embed this function".to_string(), + snippet: "fn handle_request() {}".to_string(), + }, + vector: vec![0.1, 0.2, 0.3, 0.4], + }); + index + .file_mtimes + .insert(file.clone(), SystemTime::UNIX_EPOCH); + + let estimate = index.memory_estimate(); + + assert_eq!(estimate.indexed_files, 1); + assert_eq!(estimate.entries, 1); + assert_eq!(estimate.vector_bytes, 4 * std::mem::size_of::() as u64); + assert_eq!( + estimate.embed_text_bytes, + "embed this function".len() as u64 + ); + assert_eq!( + estimate.snippet_bytes, + "fn handle_request() {}".len() as u64 + ); + assert!(estimate.metadata_bytes >= "handle_request".len() as u64); + assert_eq!( + estimate.estimated_payload_bytes, + estimate.vector_bytes + + estimate.embed_text_bytes + + estimate.snippet_bytes + + estimate.metadata_bytes + ); + } + #[test] fn symbol_kind_serialization_roundtrip_includes_file_summary_variant() { let cases = [ diff --git a/crates/aft/tests/integration/aft_search_contract_test.rs b/crates/aft/tests/integration/aft_search_contract_test.rs index 28fe9010..4221465e 100644 --- a/crates/aft/tests/integration/aft_search_contract_test.rs +++ b/crates/aft/tests/integration/aft_search_contract_test.rs @@ -312,6 +312,7 @@ fn natural_language_auto_falls_back_to_grep_while_semantic_builds() { files: Some(1), entries_done: Some(0), entries_total: Some(1), + stats: None, }; let response = response_value(handle_semantic_search( @@ -570,6 +571,7 @@ fn lexical_only_fallback_reports_more_available_when_capped_or_over_top_k() { files: Some(210), entries_done: Some(0), entries_total: Some(210), + stats: None, }; let response = response_value(handle_semantic_search(