mod background;
mod budget;
mod completions;
pub(crate) mod cursor;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod daemon_forward;
mod helpers;
mod helpers_admin;
mod helpers_archmap;
mod helpers_calls;
mod helpers_calls_scan;
#[cfg(feature = "code-search")]
mod helpers_code;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod helpers_comms;
mod helpers_compress;
#[cfg(feature = "documents")]
mod helpers_documents;
mod helpers_files;
mod helpers_git;
#[cfg(feature = "memory")]
mod helpers_governance;
mod helpers_graph;
mod helpers_grep;
mod helpers_impls;
mod helpers_intel;
#[cfg(feature = "memory")]
mod helpers_proposals;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod helpers_registry;
#[cfg(all(feature = "shells", any(unix, windows)))]
mod helpers_shells;
mod helpers_telemetry;
#[cfg(feature = "crawl")]
mod helpers_web;
mod identity;
mod kneedle;
mod lean;
mod lenient;
mod map_fingerprint;
#[cfg(any(feature = "memory", feature = "documents", feature = "code-search"))]
mod memory;
#[cfg(feature = "memory")]
pub(crate) mod memory_ops;
mod notifications;
mod prompts;
#[cfg(feature = "memory")]
pub(crate) mod proposals_ops;
mod savings;
mod state;
mod telemetry;
mod tokens;
mod tools;
mod tools_admin;
mod tools_archmap;
mod tools_code;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod tools_comms;
mod tools_compress;
mod tools_git;
mod tools_governance;
mod tools_memory;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod tools_registry;
#[cfg(all(feature = "shells", any(unix, windows)))]
mod tools_shells;
#[cfg(feature = "crawl")]
mod tools_web;
mod toon;
mod types;
mod types_admin;
mod types_archmap;
mod types_code;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod types_comms;
mod types_compress;
mod types_documents;
mod types_git;
pub(crate) mod types_governance;
mod types_graph;
mod types_impls;
pub(crate) mod types_memory;
#[cfg(all(feature = "comms", any(unix, windows)))]
mod types_registry;
#[cfg(all(feature = "shells", any(unix, windows)))]
mod types_shells;
#[cfg(feature = "crawl")]
mod types_web;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use arc_swap::ArcSwap;
use lru::LruCache;
use rmcp::ServerHandler;
use rmcp::handler::server::router::prompt::PromptRouter;
use rmcp::handler::server::tool::ToolRouter;
use rmcp::model::{
CompleteRequestParams, CompleteResult, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
PaginatedRequestParams, ServerCapabilities, ServerInfo,
};
use rmcp::tool_handler;
use tokio::sync::RwLock;
use crate::extract::FileMapL1;
use crate::lang::LangId;
use crate::store::Store;
pub(crate) use state::{Lifecycle, MapCache, ServerState};
pub mod params {
pub use rmcp::handler::server::wrapper::Parameters;
pub(crate) use super::lenient::Lenient;
pub use super::types::{
BlameFileParams, BlameSymbolParams, CommitsTouchingParams, DependentsParams, DiffFileParams, DiffOutlineParams,
FindCallersParams, FindCommitsByPathParams, FindFilesParams, FindReferencesParams, GotoDefinitionParams,
HotFilesParams, ListFilesParams, OutlineParams, RecentChangesParams, RepoInfoParams, RescanParams,
SearchDocumentsParams, SearchGitHistoryParams, SearchSymbolsParams, StatusParams, SymbolHistoryParams,
TelemetrySummaryParams, WorkingTreeStatusParams, WorkspaceGrepParams,
};
#[cfg(feature = "crawl")]
pub use super::types::{WebCrawlParams, WebMapParams, WebScrapeParams};
pub use super::types_admin::{CacheClearParams, CacheGcParams, CacheStatsParams};
pub use super::types_archmap::ArchitectureMapParams;
pub use super::types_code::{GetChunkParams, SearchCodeParams};
pub use super::types_compress::ExpandParams;
pub use super::types_governance::{
MemoryAuditParams, ProposalAcceptParams, ProposalRejectParams, ProposalsListParams, ProposalsMineParams,
};
pub use super::types_graph::CallGraphParams;
pub use super::types_impls::FindImplementationsParams;
pub use super::types_memory::{
MemoryDeleteParams, MemoryGetParams, MemoryListParams, MemoryPutParams, MemorySearchParams, Visibility,
};
#[cfg(all(feature = "shells", any(unix, windows)))]
pub use super::types_shells::{
ShellBroadcastParams, ShellCaptureParams, ShellEnv, ShellKillParams, ShellListParams, ShellSendParams,
ShellSpawnParams,
};
}
pub use params::Parameters;
pub(crate) const OUTLINE_CACHE_CAP: usize = 512;
pub(crate) struct OutlineEntry {
pub map: Arc<FileMapL1>,
pub source: Arc<Vec<u8>>,
}
pub(crate) type OutlineCache = Mutex<LruCache<(gix::ObjectId, LangId), Arc<OutlineEntry>>>;
#[derive(Clone)]
pub struct BasemindServer {
pub(crate) state: Arc<ServerState>,
#[allow(dead_code)]
tool_router: ToolRouter<Self>,
prompt_router: PromptRouter<Self>,
}
#[derive(Debug, Clone, Copy)]
pub struct ServerOptions {
pub background: bool,
pub watch: bool,
pub read_only: bool,
pub daemon_writer: bool,
pub lazy_cache: bool,
}
impl Default for ServerOptions {
fn default() -> Self {
Self {
background: true,
watch: true,
read_only: false,
daemon_writer: false,
lazy_cache: false,
}
}
}
impl BasemindServer {
pub fn new(
store: Store,
root: PathBuf,
config: Arc<crate::config::Config>,
repo: Option<Arc<crate::git::Repo>>,
git_cache: Arc<crate::git_cache::GitCache>,
) -> Self {
Self::new_with_options(store, root, config, repo, git_cache, ServerOptions::default())
}
pub fn new_oneshot(
store: Store,
root: PathBuf,
config: Arc<crate::config::Config>,
repo: Option<Arc<crate::git::Repo>>,
git_cache: Arc<crate::git_cache::GitCache>,
) -> Self {
Self::new_with_options(
store,
root,
config,
repo,
git_cache,
ServerOptions {
background: false,
watch: false,
read_only: false,
daemon_writer: false,
lazy_cache: true,
},
)
}
pub fn new_with_options(
store: Store,
root: PathBuf,
config: Arc<crate::config::Config>,
repo: Option<Arc<crate::git::Repo>>,
git_cache: Arc<crate::git_cache::GitCache>,
options: ServerOptions,
) -> Self {
let scope = repo
.as_ref()
.map(|r| crate::git::scope_key(r))
.unwrap_or_else(|| format!("path:{}", root.display()));
let agent_id = identity::resolve_agent_id(&config, &store);
let history_dir = crate::git_history::shared_history_basemind_dir(&root);
let git_history = Self::open_git_history(&root, &history_dir, repo.is_some(), &agent_id, &options);
let corpus_bytes: u64 = store.index.files.values().map(|e| e.size_bytes).sum();
let view_is_working = store.view == crate::store::VIEW_WORKING;
let fjall_index_empty = store
.index_db
.as_ref()
.map(|db| db.symbols_index_is_empty())
.unwrap_or(false);
let needs_initial_scan = (options.daemon_writer || !options.read_only)
&& view_is_working
&& (store.index.files.is_empty() || fjall_index_empty);
let defer_warm = options.background && !needs_initial_scan;
let cache = if defer_warm || options.lazy_cache {
Arc::new(MapCache::empty())
} else {
Arc::new(MapCache::build(&store))
};
tracing::info!(
files = store.index.files.len(),
corpus_bytes,
git = repo.is_some(),
scope = %scope,
deferred_warm = defer_warm,
lazy_cache = options.lazy_cache,
"code map ready for MCP server (preloaded, warming in background, or lazy)"
);
let outline_cache: Arc<OutlineCache> = Arc::new(Mutex::new(LruCache::new(
NonZeroUsize::new(OUTLINE_CACHE_CAP).expect("OUTLINE_CACHE_CAP > 0"),
)));
let telemetry_handle = Arc::new(telemetry::Telemetry::new(&store.basemind_dir));
#[cfg(feature = "crawl")]
let crawl_engine = match crate::web::build_engine(&config.crawl) {
Ok(e) => Some(e),
Err(error) => {
tracing::warn!(?error, "crawl engine init failed; web_* tools will report errors");
None
}
};
let state = Arc::new(ServerState {
store: RwLock::new(store),
root,
cache: ArcSwap::from(cache),
repo,
git_cache,
git_history,
outline_cache,
config,
telemetry: telemetry_handle,
corpus_bytes: std::sync::atomic::AtomicU64::new(corpus_bytes),
cache_generation: std::sync::atomic::AtomicU32::new(1),
scope,
agent_id,
#[cfg(any(feature = "memory", feature = "documents", feature = "code-search"))]
lance: tokio::sync::OnceCell::new(),
#[cfg(feature = "intelligence")]
embedder: tokio::sync::OnceCell::new(),
#[cfg(feature = "crawl")]
crawl_engine,
#[cfg(all(feature = "comms", any(unix, windows)))]
comms_clients: tokio::sync::Mutex::new(ahash::AHashMap::new()),
#[cfg(all(feature = "shells", any(unix, windows)))]
shell_runtime: crate::shells::ShellRuntime::new(),
log_level: std::sync::atomic::AtomicU8::new(notifications::DEFAULT_LOG_ORDINAL),
initial_scan_active: std::sync::atomic::AtomicBool::new(false),
initial_scan_ms: std::sync::atomic::AtomicU64::new(0),
cache_warming: std::sync::atomic::AtomicBool::new(defer_warm),
cache_warm_ms: std::sync::atomic::AtomicU64::new(0),
cache_ready: tokio::sync::Notify::new(),
rescan_active: std::sync::atomic::AtomicBool::new(false),
lazy_cache: options.lazy_cache,
lazy_cache_built: tokio::sync::OnceCell::new(),
read_only: options.read_only,
#[cfg(all(feature = "comms", any(unix, windows)))]
daemon_writer: options.daemon_writer,
});
if options.background {
let view_is_working = {
match state.store.try_read() {
Ok(g) => g.view == crate::store::VIEW_WORKING,
Err(_) => false,
}
};
if options.watch && (options.daemon_writer || !options.read_only) && view_is_working {
background::spawn_serve_watcher(Arc::clone(&state));
} else {
background::spawn_view_watcher(Arc::clone(&state));
}
Self::spawn_git_history_sync(&state, &history_dir);
if needs_initial_scan {
background::spawn_initial_scan(Arc::clone(&state));
} else {
if defer_warm {
background::spawn_cache_warm(Arc::clone(&state));
}
let gc_state = Arc::clone(&state);
tokio::spawn(async move {
background::run_background_gc(gc_state).await;
});
}
}
#[allow(unused_mut)]
let mut router = Self::tool_router_core()
+ Self::tool_router_archmap()
+ Self::tool_router_git()
+ Self::tool_router_memory()
+ Self::tool_router_code()
+ Self::tool_router_governance()
+ Self::tool_router_admin()
+ Self::tool_router_compress();
#[cfg(feature = "crawl")]
{
router += Self::tool_router_web();
}
#[cfg(all(feature = "comms", any(unix, windows)))]
{
router += Self::tool_router_comms();
router += Self::tool_router_registry();
}
#[cfg(all(feature = "shells", any(unix, windows)))]
{
router += Self::tool_router_shells();
}
Self {
state,
tool_router: router,
prompt_router: Self::prompt_router(),
}
}
fn open_git_history(
root: &std::path::Path,
history_dir: &std::path::Path,
has_repo: bool,
agent_id: &str,
options: &ServerOptions,
) -> Option<Arc<crate::git_history::GitHistoryIndex>> {
if !has_repo || !crate::git_history::index_enabled() {
return None;
}
#[cfg(all(feature = "comms", any(unix, windows)))]
if options.daemon_writer || crate::git_history::remote::daemon_is_up() {
let agent = crate::comms::ids::AgentId::parse(agent_id.to_string())
.inspect_err(|error| tracing::warn!(%error, "git-history: bad agent id; tools will live-walk"))
.ok()?;
return Some(Arc::new(crate::git_history::GitHistoryIndex::remote(
root.to_path_buf(),
agent,
)));
}
let _ = (root, agent_id);
if options.read_only {
return None;
}
match crate::git_history::GitHistoryIndex::open(history_dir) {
Ok(index) => Some(Arc::new(index)),
Err(error) => {
tracing::warn!(?error, "git-history index unavailable; tools will live-walk");
None
}
}
}
fn spawn_git_history_sync(state: &Arc<ServerState>, history_dir: &std::path::Path) {
let Some(index) = state.git_history.as_deref() else {
return;
};
let _ = index;
#[cfg(all(feature = "comms", any(unix, windows)))]
if index.is_daemon_backed() {
let root = state.root.clone();
let agent_id = state.agent_id.clone();
tokio::spawn(async move {
let Ok(agent) = crate::comms::ids::AgentId::parse(agent_id) else {
return;
};
match crate::git_history::remote::request_sync(root, agent).await {
Some(outcome) => tracing::info!(?outcome, "git-history index synced by the daemon"),
None => tracing::warn!("git-history index sync unavailable; history tools live-walk"),
}
});
return;
}
if let (Some(git_history), Some(repo)) = (state.git_history.clone(), state.repo.clone()) {
let history_dir = history_dir.to_path_buf();
tokio::task::spawn_blocking(move || {
match crate::git_history::builder::sync(&git_history, &repo, &history_dir) {
Ok(outcome) => tracing::info!(?outcome, "git-history index sync complete"),
Err(error) => tracing::warn!(%error, "git-history index sync failed; tools live-walk"),
}
});
}
}
pub fn tool_names(&self) -> Vec<String> {
self.tool_router
.list_all()
.into_iter()
.map(|tool| tool.name.to_string())
.collect()
}
}
#[tool_handler(router = self.tool_router.clone())]
impl ServerHandler for BasemindServer {
async fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
if lean::lean_mode_enabled() {
return Ok(lean::lean_list_tools());
}
Ok(rmcp::model::ListToolsResult {
tools: self.tool_router.list_all(),
meta: None,
next_cursor: None,
})
}
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
if lean::lean_mode_enabled() {
return lean::lean_call_tool(self, &self.tool_router, request, context).await;
}
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
self.tool_router.call(tcc).await
}
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
if lean::lean_mode_enabled() {
return lean::lean_get_tool(name);
}
self.tool_router.get(name).cloned()
}
async fn list_prompts(
&self,
_request: Option<PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<ListPromptsResult, rmcp::ErrorData> {
Ok(ListPromptsResult {
prompts: self.prompt_router.list_all(),
meta: None,
next_cursor: None,
})
}
async fn get_prompt(
&self,
request: GetPromptRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<GetPromptResult, rmcp::ErrorData> {
let prompt_context =
rmcp::handler::server::prompt::PromptContext::new(self, request.name, request.arguments, context);
self.prompt_router.get_prompt(prompt_context).await
}
#[allow(deprecated)]
async fn set_level(
&self,
request: rmcp::model::SetLevelRequestParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<(), rmcp::ErrorData> {
self.state.log_level.store(
notifications::level_ordinal(request.level),
std::sync::atomic::Ordering::Relaxed,
);
Ok(())
}
async fn complete(
&self,
request: CompleteRequestParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<CompleteResult, rmcp::ErrorData> {
self.state.await_cache_ready().await;
Ok(self.complete_argument(&request))
}
#[allow(deprecated)]
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()
.enable_completions()
.enable_logging()
.build(),
)
.with_instructions(
"basemind is the indexed context layer for this repository, served over MCP: a \
tree-sitter code map across 300+ languages (symbols, references, callers, call \
graphs, implementations), git history + blame at symbol resolution, full-text + \
semantic search, document RAG over 90+ file formats, and shared cross-session \
memory. The index lives in a machine-global cache (the platform data directory — \
`~/Library/Application Support/basemind/` on macOS, `~/.local/share/basemind/` on \
Linux; override with `BASEMIND_DATA_HOME`) keyed by workspace — nothing is written \
into the repo — and a background daemon is the sole writer, so any number of sessions on \
this repo read and write concurrently. basemind first, shell/grep/git fallback: \
prefer these tools over reading files, over grep, and over naked `git` — and use \
them for document extraction, web crawling, and code parsing too. You may be one of \
several agents in this repo: on start, check your inbox and the threads scoped to \
where you're working, and post status as you go (see Agent comms below).\n\
Context economy — these tools return paths, line numbers, and signatures, not \
file bodies, so they cost a fraction of the tokens of reading source. Default to \
them: `outline` a file before you open it (then read only the span you need); \
`search_symbols` instead of grep for a definition; `find_references` / \
`find_callers` instead of grepping call sites; `workspace_grep` instead of \
shelling out to ripgrep; `rescan` after edits instead of reconnecting. Do not \
re-read a file basemind already mapped. Same discipline beyond code: use the git \
tools (`recent_changes` / `blame_*` / `diff_*` / `commits_touching`) instead of \
shelling out to `git log`/`git blame`; `search_documents` and the documents \
pipeline for extraction, RAG, keyword + entity (NER), and summary instead of \
opening files; `web_scrape` / `web_crawl` / `web_map` for scraping, crawling, and \
sitemaps.\n\
Routing: \
\"where is X defined?\" → `search_symbols`; \
\"what calls X?\" → `find_references` (any name) or `find_callers` (specific def); \
\"shape of this file?\" → `outline` (add `l2: true` for calls + docs); \
\"what changed recently?\" → `recent_changes`, `commits_touching`, `symbol_history`; \
\"who last touched this?\" → `blame_file` / `blame_symbol`; \
\"where's the churn?\" → `hot_files`; \
\"semantic search across PDFs/docs in the repo?\" → `search_documents`; \
\"recall something the agent remembered earlier?\" → `memory_get` / `memory_list` / \
`memory_search`; \
\"remember this for later sessions?\" → `memory_put` (delete with `memory_delete`); \
\"refresh the index after editing code?\" → `rescan` (or `rescan { paths: [...] }` \
to limit to changed files); \
\"any other agents working here / leave a note for the next session?\" → \
`inbox_read` / `thread_list` / `thread_post`.\n\
\"find a file by name/path when I only remember a fragment?\" → `find_files` \
(fuzzy, fzf/fd-style).\n\
\"which repos/worktrees/branches does the daemon know?\" → `workspaces` / \
`worktrees` / `branches`; claim a worktree so sessions don't collide with \
`worktree_claim` (release with `worktree_release`).\n\
\"got a truncated result? fetch the next page?\" → pass `next_cursor` from the prior \
response back as `cursor`.\n\
\"need regex over file contents?\" → `workspace_grep`.\n\
Code-map tools: `outline`, `search_symbols`, `find_references`, `find_callers`, \
`list_files`, `find_files`, `workspace_grep`, `dependents`, `status`, `repo_info`, \
`symbol_history`. \
Coordination tools: `workspaces`, `worktrees`, `branches`, `worktree_claim`, \
`worktree_release` (advisory claims across the daemon's known worktrees). \
Git tools (inside a repo): `working_tree_status`, `recent_changes`, `commits_touching`, \
`find_commits_by_path`, `hot_files`, `diff_outline`, `diff_file`, `blame_file`, \
`blame_symbol`. \
Intelligence tools (require build with `--features documents,memory`): \
`search_documents`, `memory_put`, `memory_get`, `memory_list`, `memory_search`, \
`memory_delete`. \
Web tools (require build with `--features crawl`): `web_scrape` (one URL), \
`web_crawl` (follow links from a seed URL), `web_map` (sitemap-only discovery). \
Crawled pages land in the same LanceDB documents table as on-disk docs, scoped \
under `web:<host>` — find them later with `search_documents`. \
Agent comms (require build with `--features comms`): coordinate with other agents \
via THREADS — scoped conversations addressed by at least two of {subject, \
path-glob, members}. Threads are discovered by scope (you're a member, your cwd \
matches the thread's path-glob, or a subject filter) — never globally — and you \
must explicitly `thread_join` to participate; there is no auto-join. On start, \
`inbox_read` (front-matter only: subject / from / id — call `message_get` with an \
id for a body) and `thread_list` to see threads in scope; skim `thread_history` on \
the relevant one. `thread_start {subject, path_glob?, members?}` opens a thread \
(you're the creator/admin; a human is also admin); `thread_post {thread, subject, \
body, reply_to?}` when you begin, finish, or hit a decision, and reply \
(`reply_to`) to messages about your work — do not stay silent when collaborating. \
`inbox_ack` clears read messages. Idle threads auto-archive; `thread_archive` \
closes one explicitly. Tools: `thread_start`, `thread_list`, `thread_join`, \
`thread_leave`, `thread_members`, `thread_add_member`, `thread_remove_member`, \
`thread_archive`, `thread_post`, `thread_history`, `message_get`, `inbox_read`, \
`inbox_ack`, `agent_register`, `agent_list`. \
All paths are repository-relative with forward-slash separators. \
If a tool reports \"no indexed files\", run `basemind scan` in the repo first.",
)
}
}
#[cfg(test)]
#[path = "lazy_cache_tests.rs"]
mod lazy_cache_tests;
#[cfg(test)]
mod map_cache_tests {
use super::*;
use std::fs;
use std::path::Path;
fn sym_names(cache: &MapCache, rel: &str) -> Vec<String> {
let key = crate::path::RelPath::from(rel);
cache
.by_path
.get(&key)
.map(|l1| l1.symbols.iter().map(|s| s.name.clone()).collect())
.unwrap_or_default()
}
#[test]
fn lifecycle_from_flags_applies_precedence() {
assert_eq!(Lifecycle::from_flags(false, false, false), Lifecycle::Ready);
assert_eq!(Lifecycle::from_flags(false, false, true), Lifecycle::Rescanning);
assert_eq!(Lifecycle::from_flags(false, true, true), Lifecycle::WarmingUp);
assert_eq!(Lifecycle::from_flags(true, true, true), Lifecycle::BuildingIndex);
assert_eq!(Lifecycle::from_flags(true, false, false), Lifecycle::BuildingIndex);
}
#[test]
fn lifecycle_notice_maps_state_to_tag_and_retry() {
assert!(types::LifecycleNotice::for_state(Lifecycle::Ready).is_none());
let warming = types::LifecycleNotice::for_state(Lifecycle::WarmingUp).expect("warming notice");
assert_eq!(warming.state, "warming_up");
assert!(warming.retry, "warming asks the caller to retry for complete results");
let building = types::LifecycleNotice::for_state(Lifecycle::BuildingIndex).expect("building notice");
assert_eq!(building.state, "building_index");
assert!(building.retry);
let rescanning = types::LifecycleNotice::for_state(Lifecycle::Rescanning).expect("rescan notice");
assert_eq!(rescanning.state, "rescanning");
assert!(!rescanning.retry, "rescan results are usable, no retry required");
}
#[test]
fn with_delta_patches_updated_and_removed_paths_only() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::write(root.join("a.rs"), b"pub fn alpha() {}\n").unwrap();
fs::write(root.join("b.rs"), b"pub fn beta() {}\n").unwrap();
let cfg = crate::config::ConfigV1::with_defaults();
let mut store = crate::store::Store::open(root, crate::store::VIEW_WORKING).unwrap();
crate::scanner::scan(
root,
&mut store,
&cfg,
crate::scanner::ScanSource::WorkingTree,
crate::scanner::EmbedMode::Inline,
)
.unwrap();
let cache = MapCache::build(&store);
assert_eq!(sym_names(&cache, "a.rs"), vec!["alpha".to_string()]);
assert_eq!(sym_names(&cache, "b.rs"), vec!["beta".to_string()]);
assert!(cache.calls.is_none() && cache.impls.is_none());
fs::write(root.join("a.rs"), b"pub fn alpha2() {}\npub fn alpha3() {}\n").unwrap();
let report = crate::scanner::scan_paths(
root,
&mut store,
&cfg,
&[root.join("a.rs")],
crate::scanner::EmbedMode::Inline,
)
.unwrap();
assert_eq!(report.stats.updated, 1);
let updated = vec![crate::path::RelPath::from("a.rs")];
let next = cache.with_delta(&store, &updated, &[]);
assert_eq!(
sym_names(&next, "a.rs"),
vec!["alpha2".to_string(), "alpha3".to_string()],
"updated path reflects fresh L1"
);
assert_eq!(
sym_names(&next, "b.rs"),
vec!["beta".to_string()],
"untouched path preserved without re-reading its blob"
);
let removed = vec![crate::path::RelPath::from("b.rs")];
let after = next.with_delta(&store, &[], &removed);
assert!(
!after.by_path.contains_key(&crate::path::RelPath::from("b.rs")),
"removed path dropped from by_path"
);
assert!(
after.by_path.contains_key(&crate::path::RelPath::from("a.rs")),
"other path kept"
);
assert!(
!after.imports_index.iter().any(|(p, _)| p == Path::new("b.rs")),
"imports_index must not retain a removed path"
);
}
}