use std::sync::OnceLock;
use rag_rat_core::OutputFormat;
use super::*;
use crate::cli::{
BriefArgs, ClustersArgs, EvalArgs, GithubArgs, GithubCommand, HookAction, HooksArgs,
ImportantSymbolsArgs, IndexArgs, MaintenanceArgs, MemoryArgs, MemoryCommand, ModelsArgs,
ModelsCommand, OracleArgs, OracleCommand, OracleRunArgs, OracleStatusArgs, QueryArgs,
ReconcileArgs,
};
static OUTPUT_FORMAT: OnceLock<OutputFormat> = OnceLock::new();
pub(crate) fn set_output_format(format: OutputFormat) {
let _ = OUTPUT_FORMAT.set(format);
}
pub(crate) fn output_format() -> OutputFormat {
OUTPUT_FORMAT.get().copied().unwrap_or_default()
}
pub(crate) fn index(config: &Config, args: &IndexArgs) -> anyhow::Result<()> {
if args.watch {
return run_watch(config.clone());
}
let _lock = rag_rat_core::locks::FileLock::acquire_blocking(
&rag_rat_core::locks::write_lock_path(&config.database),
)?;
let db = if args.full {
IndexDatabase::rebuild_with_progress(config, render_index_progress)?
} else if args.discover {
IndexDatabase::index_discover_with_progress(config, render_index_progress)?
} else {
IndexDatabase::index_changed_with_progress(config, render_index_progress)?
};
if let Err(err) = db.memory_validate() {
eprintln!("warning: repo-memory re-validation failed: {err}");
}
let doctor_count = db.memory_doctor().map(|entries| entries.len()).unwrap_or(0);
if doctor_count > 0 {
eprintln!("⚠ {doctor_count} repo memories need re-anchoring — run 'rag-rat memory doctor'");
}
print_output(&db.status(&config.database)?)
}
pub(crate) fn query(config: &Config, args: &QueryArgs) -> anyhow::Result<()> {
let query = args.query.join(" ");
if query.trim().is_empty() {
anyhow::bail!("query command needs a search string");
}
let db = open_index(config)?;
if args.explain {
print_query_explain(&db.search_explain(&query, 10, false)?);
return Ok(());
}
print_output(&db.search(&query, 10, false)?)
}
pub(crate) fn brief(config: &Config, args: &BriefArgs) -> anyhow::Result<()> {
let db = open_index(config)?;
let mode = rag_rat_core::query::repo_brief::RepoBriefMode::parse(args.mode.as_deref())?;
print_output(&db.repo_brief(rag_rat_core::query::repo_brief::RepoBriefOptions {
mode,
limit: args.limit.unwrap_or(10),
include_generated: args.include_generated,
include_memories: !args.no_memories,
})?)
}
pub(crate) fn clusters(config: &Config, args: &ClustersArgs) -> anyhow::Result<()> {
let db = open_index(config)?;
print_output(&db.repo_clusters(rag_rat_core::query::clusters::RepoClustersOptions {
limit: args.limit.unwrap_or(10),
include_generated: args.include_generated,
include_memories: !args.no_memories,
min_cluster_size: args.min_cluster_size.unwrap_or(2),
})?)
}
pub(crate) fn important_symbols(
config: &Config,
args: &ImportantSymbolsArgs,
) -> anyhow::Result<()> {
let db = open_index(config)?;
let mut result = db.important_symbols(rag_rat_core::index::ImportantSymbolsRequest {
limit: args.limit.unwrap_or(20) as usize,
personalize: args.personalize.clone(),
auto_seed_from_diff: false,
})?;
apply_auto_run_ranking_hint(&mut result, config);
print_output(&result)
}
pub(crate) fn apply_auto_run_ranking_hint(
result: &mut rag_rat_core::query::pagerank::ImportantSymbolsResult,
config: &Config,
) {
if config.oracle.auto_run && result.ranking_hint.is_some() {
result.ranking_hint =
Some(rag_rat_core::query::pagerank::RANKING_HINT_AUTO_RUN.to_string());
}
}
pub(crate) fn dump_config(config: &Config) -> anyhow::Result<()> {
let targets = config
.targets
.iter()
.map(|target| {
serde_json::json!({
"name": target.name,
"language": target.language.as_str(),
"directories": target.directories,
"include": target.include,
"exclude": target.exclude,
"kind": target.kind.as_str(),
})
})
.collect::<Vec<_>>();
print_output(&serde_json::json!({
"root": config.root,
"database": config.database,
"local_ai": {
"embedding": {
"runtime": {
"batch_size": config.local_ai.embedding.runtime.batch_size,
"ort_threads": config.local_ai.embedding.runtime.ort_threads,
"omp_threads": config.local_ai.embedding.runtime.omp_threads,
"max_embedding_chars": config.local_ai.embedding.runtime.max_embedding_chars,
}
}
},
"targets": targets,
}))
}
pub(crate) fn version_check(config: &Config) -> anyhow::Result<()> {
use rag_rat_core::version_check;
if !config.version_check.enabled {
return print_output(&serde_json::json!({
"enabled": false,
"current_version": version_check::current_version(),
"note": "version checking is disabled ([version_check] enabled = false in rag-rat.toml)",
}));
}
let cached = version_check::refresh(&config.database)
.or_else(|| version_check::read_cache(&config.database));
print_output(&version_check::build_status(version_check::current_version(), cached.as_ref()))
}
pub(crate) fn eval(config: &Config, args: &EvalArgs) -> anyhow::Result<()> {
let options = rag_rat_core::eval::EvalOptions {
queries_path: args
.queries
.clone()
.unwrap_or_else(|| default_eval_path(config, "queries.toml")),
expected_path: args
.expected
.clone()
.unwrap_or_else(|| default_eval_path(config, "expected_hits.toml")),
update_baseline: args.update_baseline,
scip_path: args.scip.clone().or_else(|| {
let default = default_eval_path(config, "oracle.scip");
default.exists().then_some(default)
}),
};
let report = rag_rat_core::eval::run(config, &options)?;
if output_format() == OutputFormat::Json || options.update_baseline {
print_output(&report)?;
} else {
print_eval_summary(&report);
}
if !report.pass {
anyhow::bail!(
"eval failed: stale_current_source_violations={}, failed_queries={}",
report.metrics.stale_current_source_violations,
report.results.iter().filter(|result| !result.passed).count()
);
}
Ok(())
}
pub(crate) fn default_eval_path(config: &Config, file_name: &str) -> PathBuf {
config.root.join("evals").join(file_name)
}
pub(crate) fn oracle(config: &Config, args: &OracleArgs) -> anyhow::Result<()> {
match &args.command {
OracleCommand::Run(run_args) => oracle_run(config, run_args),
OracleCommand::Status(status_args) => {
let db = open_index(config)?;
oracle_status(&db, status_args)
},
}
}
pub(crate) fn with_oracle_write_lock<T>(
config: &Config,
body: impl FnOnce(&IndexDatabase) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
let _lock = rag_rat_core::locks::FileLock::acquire_blocking(
&rag_rat_core::locks::write_lock_path(&config.database),
)?;
let db = open_index(config)?;
body(&db)
}
fn oracle_run(config: &Config, args: &OracleRunArgs) -> anyhow::Result<()> {
let tool = args.tool.core();
if let Some(scip_path) = &args.scip {
let scip_bytes = fs::read(scip_path).map_err(|err| {
anyhow::anyhow!("failed to read SCIP index {}: {err}", scip_path.display())
})?;
let tool_version = format!(
"scip-file:{}@{}",
scip_path.file_name().and_then(|n| n.to_str()).unwrap_or("index.scip"),
rag_rat_core::index::oracle::scip_content_fingerprint(&scip_bytes),
);
let report = with_oracle_write_lock(config, |db| {
db.run_oracle_from_scip(tool, &tool_version, &scip_bytes)
})?;
return print_output(&serde_json::json!({
"outcome": "completed",
"tool": tool.as_db_str(),
"tool_version": tool_version,
"report": report,
}));
}
if let rag_rat_core::index::oracle::ToolAvailability::Blocked { tool, program, hint } =
rag_rat_core::index::oracle::probe_oracle_tool(tool)
{
eprintln!("oracle: {hint}");
return print_output(&rag_rat_core::index::oracle::OracleRunOutcome::Blocked {
tool,
program,
hint,
});
}
let (started_at_ms, pre_spawn_sha) = with_oracle_write_lock(config, |db| {
Ok((crate::now_epoch_ms(), db.oracle_pre_spawn_snapshot()?))
})?;
let scip_output = config
.database
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(std::env::temp_dir)
.join(format!("rag-rat-oracle-{}.scip", std::process::id()));
let production =
rag_rat_core::index::oracle::produce_scip_with_tool(tool, &config.root, &scip_output);
let _ = fs::remove_file(&scip_output);
match production? {
rag_rat_core::index::oracle::ScipProduction::Blocked { tool, program, hint } => {
eprintln!("oracle: {hint}");
print_output(&rag_rat_core::index::oracle::OracleRunOutcome::Blocked {
tool,
program,
hint,
})
},
rag_rat_core::index::oracle::ScipProduction::Produced {
version,
bytes,
production_sha,
} => {
let report = with_oracle_write_lock(config, |db| {
db.run_oracle_at(
tool,
&version,
&bytes,
rag_rat_core::index::OracleShaSnapshots {
production: Some(&production_sha),
pre_spawn: Some(&pre_spawn_sha),
},
started_at_ms,
)
})?;
print_output(&serde_json::json!({
"outcome": "completed",
"tool": tool.as_db_str(),
"tool_version": version,
"report": report,
}))
},
}
}
fn oracle_status(db: &IndexDatabase, args: &OracleStatusArgs) -> anyhow::Result<()> {
let tools: Vec<rag_rat_core::index::oracle::OracleTool> = match args.tool {
Some(tool) => vec![tool.core()],
None => rag_rat_core::index::oracle::OracleTool::ALL.to_vec(),
};
let mut entries = Vec::with_capacity(tools.len());
for tool in tools {
let availability = db.probe_oracle_tool(tool);
let status = match db.latest_oracle_run_version(tool)? {
Some(version) => Some(db.oracle_status(tool, &version)?),
None => None,
};
entries.push(serde_json::json!({
"tool": tool.as_db_str(),
"tool_available": availability,
"verdicts": status,
}));
}
print_output(&entries)
}
pub(crate) fn models(config: &Config, args: &ModelsArgs) -> anyhow::Result<()> {
let db = open_index(config)?;
match &args.command {
None | Some(ModelsCommand::List) => print_output(&db.list_models()?),
Some(ModelsCommand::Install { model_id }) => print_output(&db.install_model(model_id)?),
}
}
pub(crate) fn reconcile(config: &Config, args: &ReconcileArgs) -> anyhow::Result<()> {
let db = open_index(config)?;
if args.plan {
let plan = db.reconcile_plan()?;
if output_format() == OutputFormat::Json {
print_output(&plan)?;
} else {
print_reconcile_plan(&plan);
}
return Ok(());
}
let options = rag_rat_core::index::ai::ReconcileOptions {
limit: args.limit,
batch_size: args.batch_size.or(Some(config.local_ai.embedding.runtime.batch_size)),
force: args.force,
until_clean: args.until_clean,
changed_first: args.changed_first,
max_seconds: args.max_seconds,
max_embedding_chars: args
.max_embedding_chars
.unwrap_or(config.local_ai.embedding.runtime.max_embedding_chars),
intra_threads: config.local_ai.embedding.runtime.ort_threads.map(|n| n as usize),
};
let report = db.reconcile_with_options_progress(options, render_reconcile_progress)?;
let non_current = db.memory_anchor_health().map(|h| h.stale + h.gone).unwrap_or(0);
if non_current > 0 {
eprintln!("⚠ {non_current} repo memories need re-anchoring — run 'rag-rat memory doctor'");
}
print_output(&report)
}
pub(crate) fn run_watch(config: Config) -> anyhow::Result<()> {
let Some(_watcher) = rag_rat_core::watch::Watcher::spawn(config.clone()) else {
anyhow::bail!("watcher is disabled ([watch] enabled = false or RAG_RAT_NO_WATCH set)");
};
eprintln!("rag-rat: watching {} for changes (Ctrl-C to stop)", config.root.display());
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
pub(crate) fn apply_embedding_runtime_env(runtime: &EmbeddingRuntimeConfig) {
set_env_if_absent("OMP_NUM_THREADS", runtime.omp_threads);
}
pub(crate) fn set_env_if_absent(key: &str, value: Option<u32>) {
let Some(value) = value else {
return;
};
if env::var_os(key).is_some() {
return;
}
unsafe {
env::set_var(key, value.to_string());
}
}
pub(crate) fn doctor(config: &Config) -> anyhow::Result<()> {
let schema = IndexDatabase::migration_check(&config.database)?;
let (index, discovery, storage) =
if schema.state == rag_rat_core::index::schema::SchemaState::Compatible {
let db = IndexDatabase::open_config(config)?;
(
Some(serde_json::to_value(db.status(&config.database)?)?),
Some(serde_json::to_value(db.discovery_status(config)?)?),
Some(serde_json::to_value(db.storage_status()?)?),
)
} else {
(None, None, None)
};
print_output(&serde_json::json!({
"config_root": config.root,
"database": config.database,
"schema": schema,
"storage": storage,
"discovery": discovery,
"targets": config.targets.iter().map(|target| serde_json::json!({
"name": target.name,
"language": target.language.as_str(),
"directories": target.directories,
"kind": target.kind.as_str(),
})).collect::<Vec<_>>(),
"index": index,
"mcp": {
"transport": "stdio",
"tools": rag_rat_mcp::tools::TOOL_NAMES,
"source_read_only": true,
"index_writes": "sqlite_auto_heal"
}
}))
}
fn symbol_bind_target(
hit: &rag_rat_core::query::symbol::SymbolHit,
) -> rag_rat_core::query::memory::RepoMemoryBindTarget {
rag_rat_core::query::memory::RepoMemoryBindTarget {
symbol_id: Some(hit.symbol_id),
logical_symbol_id: hit.logical_symbol_id,
..Default::default()
}
}
fn path_bind_target(path: String) -> rag_rat_core::query::memory::RepoMemoryBindTarget {
rag_rat_core::query::memory::RepoMemoryBindTarget { path: Some(path), ..Default::default() }
}
fn dir_bind_target(dir: String) -> rag_rat_core::query::memory::RepoMemoryBindTarget {
rag_rat_core::query::memory::RepoMemoryBindTarget { dir: Some(dir), ..Default::default() }
}
fn chunk_bind_target(chunk_id: i64) -> rag_rat_core::query::memory::RepoMemoryBindTarget {
rag_rat_core::query::memory::RepoMemoryBindTarget {
chunk_id: Some(chunk_id),
..Default::default()
}
}
pub(crate) fn memory(config: &Config, args: &MemoryArgs) -> anyhow::Result<()> {
match &args.command {
MemoryCommand::Doctor => {
let db = open_index(config)?;
let entries = db.memory_doctor()?;
if output_format() == OutputFormat::Json {
print_output(&entries)?;
let any_gone = entries.iter().any(|e| e.anchor_status == "gone");
if any_gone {
anyhow::bail!("one or more memories have gone anchors");
}
return Ok(());
}
if entries.is_empty() {
eprintln!("All active memory anchors are current.");
return Ok(());
}
let mut any_gone = false;
for entry in &entries {
eprintln!("[{}] {} ({})", entry.anchor_status, entry.title, entry.memory_id);
eprintln!(" binding: {} {}", entry.binding_kind, entry.binding_id);
if entry.candidates.is_empty() {
if entry.anchor_status == "gone" {
eprintln!(
" -> code appears deleted; rag-rat memory mark-obsolete {}",
entry.memory_id
);
}
} else {
for candidate in &entry.candidates {
eprintln!(
" rag-rat memory rebind {} --symbol-path {}",
entry.memory_id, candidate
);
}
}
if entry.anchor_status == "gone" {
any_gone = true;
}
}
if any_gone {
anyhow::bail!("one or more memories have gone anchors");
}
Ok(())
},
MemoryCommand::Rebind { memory_id, symbol, symbol_path, symbol_id, path, chunk, dir } => {
let db = open_index(config)?;
let bind = if symbol.is_some() || symbol_path.is_some() || symbol_id.is_some() {
let selector = rag_rat_core::query::symbol::SymbolSelector {
logical_symbol_id: None,
symbol_id: *symbol_id,
symbol_path: symbol_path.clone(),
symbol: symbol.clone(),
language: None,
allow_ambiguous: false,
limit: 10,
};
let label = symbol
.as_deref()
.or(symbol_path.as_deref())
.map(str::to_string)
.unwrap_or_else(|| format!("#{}", symbol_id.unwrap_or_default()));
match db.select_symbol_for_bind(&selector)? {
Ok(Some(hit)) => symbol_bind_target(&hit),
Ok(None) => anyhow::bail!("symbol `{label}` not found"),
Err(disambiguation) => anyhow::bail!(
"symbol `{label}` is ambiguous — disambiguate with one of:\n{}",
disambiguation
.candidates
.iter()
.map(|c| format!(
" --symbol-id {} ({} in {})",
c.symbol_id, c.qualified_name, c.path
))
.collect::<Vec<_>>()
.join("\n")
),
}
} else if let Some(path) = path {
path_bind_target(path.clone())
} else if let Some(chunk_id) = chunk {
chunk_bind_target(*chunk_id)
} else if let Some(dir) = dir {
dir_bind_target(dir.clone())
} else {
anyhow::bail!(
"memory rebind needs one of --symbol <name>, --symbol-path <path::name>, \
--symbol-id <id>, --path <path>, --chunk <id>, or --dir <dir>"
);
};
print_output(&db.memory_rebind(memory_id, bind)?)
},
MemoryCommand::List { kind } => {
let db = open_index(config)?;
let summaries = db.memory_list(kind.as_deref())?;
if output_format() == OutputFormat::Json {
return print_output(&summaries);
}
if summaries.is_empty() {
eprintln!("No memories found.");
return Ok(());
}
for s in &summaries {
println!(
"{} [{}/{}] {} ({}:{})",
s.memory_id, s.kind, s.status, s.title, s.binding_kind, s.binding_id
);
}
Ok(())
},
MemoryCommand::Show { memory_id } => {
let db = open_index(config)?;
let Some(memory) = db.memory_get(memory_id)? else {
anyhow::bail!("memory `{memory_id}` not found");
};
if output_format() == OutputFormat::Json {
return print_output(&memory);
}
println!("Title: {}", memory.title);
println!("Kind: {} / {} / {}", memory.kind, memory.status, memory.confidence);
println!();
println!("{}", memory.body);
if !memory.bindings.is_empty() {
println!();
println!("Bindings:");
for b in &memory.bindings {
println!(" {} {} [{}]", b.binding_kind, b.binding_id, b.anchor_status);
}
}
Ok(())
},
}
}
pub(crate) fn github(config: &Config, args: &GithubArgs) -> anyhow::Result<()> {
match &args.command {
GithubCommand::Sync { from_refs, issue, offline } => {
let db = open_index(config)?;
let report = if let Some(issue) = issue {
db.github_sync_issue(issue, *offline)?
} else if *from_refs {
db.github_sync_from_refs_with_progress(*offline, render_github_sync_progress)?
} else {
anyhow::bail!("github sync needs --from-refs or --issue <owner/repo#number>");
};
print_output(&report)
},
}
}
pub(crate) fn hooks(config: &Config, args: &HooksArgs) -> anyhow::Result<()> {
if args.claude {
return claude_hooks(config, args.action.as_str(), args.global);
}
let git = git_paths(&config.root)?;
match args.action {
HookAction::Install => {
fs::create_dir_all(&git.hooks_dir)?;
let mut installed = Vec::new();
for hook in MANAGED_HOOKS {
install_hook(&git.hooks_dir, hook)?;
installed.push(*hook);
}
print_output(&serde_json::json!({
"status": "installed",
"repo_root": git.worktree_root,
"git_dir": git.git_dir,
"git_common_dir": git.git_common_dir,
"hooks_dir": git.hooks_dir,
"hooks": installed,
}))
},
HookAction::Uninstall => {
let mut removed = Vec::new();
let mut kept = Vec::new();
for hook in MANAGED_HOOKS {
let path = git.hooks_dir.join(hook);
if !path.exists() {
continue;
}
if is_rag_rat_hook(&path)? {
fs::remove_file(&path)?;
removed.push(*hook);
} else {
kept.push(*hook);
}
}
print_output(&serde_json::json!({
"status": "uninstalled",
"hooks_dir": git.hooks_dir,
"removed": removed,
"kept_unmanaged": kept,
}))
},
HookAction::Status => {
let hooks = MANAGED_HOOKS
.iter()
.map(|hook| {
let path = git.hooks_dir.join(hook);
let managed = is_rag_rat_hook(&path).unwrap_or(false);
serde_json::json!({
"name": hook,
"path": path,
"exists": path.exists(),
"managed": managed,
})
})
.collect::<Vec<_>>();
print_output(&serde_json::json!({
"repo_root": git.worktree_root,
"git_dir": git.git_dir,
"git_common_dir": git.git_common_dir,
"hooks_dir": git.hooks_dir,
"hooks": hooks,
}))
},
}
}
pub(crate) fn claude_hooks(config: &Config, subcommand: &str, global: bool) -> anyhow::Result<()> {
let path = claude_settings::settings_path(&config.root, global)?;
let mut settings = claude_settings::read_settings(&path)?;
match subcommand {
"install" => {
let changed = claude_settings::merge_hook_entries(&mut settings);
if changed {
claude_settings::write_settings(&path, &settings)?;
}
print_output(&serde_json::json!({
"status": if changed { "installed" } else { "already_installed" },
"settings_path": path,
"matchers": ["Grep", "Bash"],
}))
},
"uninstall" => {
let changed = claude_settings::remove_hook_entries(&mut settings);
if changed {
claude_settings::write_settings(&path, &settings)?;
}
print_output(&serde_json::json!({
"status": if changed { "uninstalled" } else { "not_installed" },
"settings_path": path,
}))
},
"status" => {
let status = claude_settings::hook_status(&settings);
print_output(&serde_json::json!({
"settings_path": path,
"pretooluse_installed": status.pretooluse,
"session_start_installed": status.session_start,
}))
},
other => anyhow::bail!("unknown hooks subcommand `{other}`"),
}
}
pub(crate) fn maintenance(config: &Config, args: &MaintenanceArgs) -> anyhow::Result<()> {
let trigger = args.trigger.clone().unwrap_or_else(|| "manual".to_string());
let max_seconds = args.max_seconds.unwrap_or(DEFAULT_MAINTENANCE_SECONDS);
let branch_checkout = args.branch_checkout.clone();
let old_head = args.old_head.clone();
let new_head = args.new_head.clone();
let started = Instant::now();
if trigger == "post-checkout" && branch_checkout.as_deref() == Some("0") {
print_output(&serde_json::json!({
"trigger": trigger,
"status": "skipped",
"reason": "file checkout",
"old_head": old_head,
"new_head": new_head,
"branch_checkout": branch_checkout,
}))?;
return Ok(());
}
let _lock = rag_rat_core::locks::FileLock::acquire_blocking(
&rag_rat_core::locks::write_lock_path(&config.database),
)?;
let db = IndexDatabase::index_discover_with_progress(config, render_index_progress)?;
let elapsed = started.elapsed().as_secs();
let remaining_seconds = max_seconds.saturating_sub(elapsed);
let reconcile_report = if remaining_seconds > 0 {
let options = rag_rat_core::index::ai::ReconcileOptions {
limit: None,
batch_size: Some(config.local_ai.embedding.runtime.batch_size),
force: false,
until_clean: false,
changed_first: true,
max_seconds: Some(remaining_seconds),
max_embedding_chars: config.local_ai.embedding.runtime.max_embedding_chars,
intra_threads: config.local_ai.embedding.runtime.ort_threads.map(|n| n as usize),
};
Some(db.reconcile_with_options_progress(options, render_reconcile_progress)?)
} else {
None
};
let gc_report = db.garbage_collect().ok();
let memory_validation = db.memory_validate().ok();
let plan = db.reconcile_plan()?;
print_output(&serde_json::json!({
"trigger": trigger,
"status": "complete",
"old_head": old_head,
"new_head": new_head,
"branch_checkout": branch_checkout,
"max_seconds": max_seconds,
"elapsed_seconds": started.elapsed().as_secs_f64(),
"reconcile": reconcile_report,
"gc": gc_report,
"memory_validation": memory_validation,
"remaining_backlog": {
"model": plan.embeddings.model_id,
"current": plan.embeddings.current,
"missing": plan.embeddings.missing,
"stale": plan.embeddings.stale,
"failed_retryable": plan.embeddings.failed_retryable,
"failed_waiting": plan.embeddings.failed_waiting,
"blocked": plan.embeddings.blocked,
"skipped": plan.embeddings.skipped_total,
"missing_by_priority": plan.embeddings.missing_by_priority,
"skipped_by_policy": plan.embeddings.skipped_by_policy,
}
}))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::time::Duration;
use rag_rat_core::config::{ResolvedTarget, TargetKind};
use rag_rat_core::language::Language;
use rag_rat_core::locks::{FileLock, write_lock_path};
use rag_rat_core::{Config, IndexDatabase};
use crate::cli::{OracleArgs, OracleCommand, OracleRunArgs, OracleToolArg};
static N: AtomicU64 = AtomicU64::new(0);
fn temp_config() -> (PathBuf, Config) {
let root = std::env::temp_dir().join(format!(
"rag-rat-cli-oracle-lock-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "fn caller() { target(); } fn target() {}\n")
.unwrap();
let config = Config {
root: root.clone(),
database: root.join(".rag-rat/index.sqlite"),
targets: vec![ResolvedTarget {
name: "rust".to_string(),
language: Language::Rust,
directories: vec![PathBuf::from("src")],
include: vec!["src/".to_string()],
exclude: Vec::new(),
kind: TargetKind::Source,
}],
local_ai: Default::default(),
watch: Default::default(),
version_check: Default::default(),
oracle: Default::default(),
};
(root, config)
}
fn run_args() -> OracleArgs {
OracleArgs {
command: OracleCommand::Run(OracleRunArgs {
tool: OracleToolArg::RustAnalyzer,
scip: None, }),
}
}
#[test]
fn oracle_run_blocks_on_write_lock() {
let (root, config) = temp_config();
IndexDatabase::rebuild(&config).unwrap();
let scip_path = root.join("empty.scip");
std::fs::write(&scip_path, []).unwrap();
let mut args = run_args();
if let OracleCommand::Run(run) = &mut args.command {
run.scip = Some(scip_path);
}
let lock = FileLock::acquire_blocking(&write_lock_path(&config.database)).unwrap();
let (tx, rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
let result = super::oracle(&config, &args);
let _ = tx.send(result.is_ok());
});
assert!(
rx.recv_timeout(Duration::from_millis(300)).is_err(),
"oracle run completed while the write lock was held — it must block on the lock"
);
drop(lock);
let ok =
rx.recv_timeout(Duration::from_secs(20)).expect("oracle run completes after unlock");
assert!(ok, "oracle run should succeed once the lock is free");
handle.join().unwrap();
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn oracle_run_releases_write_lock_after_completion() {
let (root, config) = temp_config();
IndexDatabase::rebuild(&config).unwrap();
let scip_path = root.join("empty.scip");
std::fs::write(&scip_path, []).unwrap();
let mut args = run_args();
if let OracleCommand::Run(run) = &mut args.command {
run.scip = Some(scip_path);
}
super::oracle(&config, &args).unwrap();
let lock = FileLock::try_acquire(&write_lock_path(&config.database)).unwrap();
assert!(lock.is_some(), "oracle run must release the write lock when it returns");
let _ = std::fs::remove_dir_all(&root);
}
}