use anyhow::{Context, Result};
use super::super::commands::BatchInput;
use super::super::BatchContext;
use crate::cli::validate_finite_f32;
pub(in crate::cli::batch) struct GatherParams<'a> {
pub query: &'a str,
pub expand: usize,
pub direction: cqs::GatherDirection,
pub limit: usize,
pub tokens: Option<usize>,
pub ref_name: Option<&'a str>,
}
pub(in crate::cli::batch) fn dispatch_gather(
ctx: &BatchContext,
params: &GatherParams<'_>,
) -> Result<serde_json::Value> {
let GatherParams {
query,
expand,
direction,
limit,
tokens,
ref_name,
} = params;
let (expand, direction, limit, tokens, ref_name) =
(*expand, *direction, *limit, *tokens, *ref_name);
let _span = tracing::info_span!("batch_gather", query, ?ref_name).entered();
let embedder = ctx.embedder()?;
let opts = cqs::GatherOptions {
expand_depth: expand.clamp(0, 5),
direction,
limit: limit.clamp(1, 100),
..cqs::GatherOptions::default()
};
let mut result = if let Some(rn) = ref_name {
let query_embedding = embedder
.embed_query(query)
.context("Failed to embed query")?;
ctx.get_ref(rn)?;
let ref_idx = ctx
.borrow_ref(rn)
.ok_or_else(|| anyhow::anyhow!("Reference '{}' not loaded", rn))?;
let index = ctx.vector_index()?;
let index = index.as_deref();
cqs::gather_cross_index_with_index(
&ctx.store(),
&ref_idx,
&query_embedding,
query,
&opts,
&ctx.root,
index,
)?
} else {
cqs::gather(&ctx.store(), embedder, query, &opts, &ctx.root)?
};
let token_info: Option<(usize, usize)> = if let Some(budget) = tokens {
let embedder = ctx.embedder()?;
let chunks = std::mem::take(&mut result.chunks);
let (packed, used) = crate::cli::commands::pack_gather_chunks(
chunks,
embedder,
budget,
crate::cli::commands::JSON_OVERHEAD_PER_RESULT,
);
result.chunks = packed;
Some((used, budget))
} else {
None
};
let output = crate::cli::commands::build_gather_output(&result, query, token_info);
Ok(serde_json::to_value(&output)?)
}
pub(in crate::cli::batch) fn dispatch_notes(
ctx: &BatchContext,
warnings: bool,
patterns: bool,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_notes", warnings, patterns).entered();
let notes = ctx.notes();
let filtered: Vec<_> = notes
.iter()
.filter(|n| {
if warnings {
n.is_warning()
} else if patterns {
n.is_pattern()
} else {
true
}
})
.map(|n| {
serde_json::json!({
"text": n.text,
"sentiment": n.sentiment,
"sentiment_label": n.sentiment_label(),
"mentions": n.mentions,
})
})
.collect();
Ok(serde_json::json!({
"notes": filtered,
"total": filtered.len(),
}))
}
pub(in crate::cli::batch) fn dispatch_task(
ctx: &BatchContext,
description: &str,
limit: usize,
tokens: Option<usize>,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_task", description).entered();
let embedder = ctx.embedder()?;
let limit = limit.clamp(1, 10);
let graph = ctx.call_graph()?;
let test_chunks = ctx.test_chunks()?;
let result = cqs::task_with_resources(
&ctx.store(),
embedder,
description,
&ctx.root,
limit,
&graph,
&test_chunks,
)?;
let json = if let Some(budget) = tokens {
crate::cli::commands::task::task_to_budgeted_json(&result, embedder, budget)
} else {
serde_json::to_value(&result)?
};
Ok(json)
}
pub(in crate::cli::batch) fn dispatch_scout(
ctx: &BatchContext,
query: &str,
limit: usize,
tokens: Option<usize>,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_scout", query).entered();
let embedder = ctx.embedder()?;
let limit = limit.clamp(1, 50);
let result = cqs::scout(&ctx.store(), embedder, query, &ctx.root, limit)?;
let Some(budget) = tokens else {
return Ok(serde_json::to_value(&result)?);
};
let named_items = crate::cli::commands::scout_scored_names(&result);
let (content_map, used) =
crate::cli::commands::fetch_and_pack_content(&ctx.store(), embedder, &named_items, budget);
let mut json = serde_json::to_value(&result)?;
crate::cli::commands::inject_content_into_scout_json(&mut json, &content_map);
crate::cli::commands::inject_token_info(&mut json, Some((used, budget)));
Ok(json)
}
pub(in crate::cli::batch) fn dispatch_where(
ctx: &BatchContext,
description: &str,
limit: usize,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_where", description).entered();
let embedder = ctx.embedder()?;
let limit = limit.clamp(1, 10);
let result = cqs::suggest_placement(&ctx.store(), embedder, description, limit)?;
let output = crate::cli::commands::build_where_output(&result, description, &ctx.root);
Ok(serde_json::to_value(&output)?)
}
pub(in crate::cli::batch) fn dispatch_drift(
ctx: &BatchContext,
reference: &str,
threshold: f32,
min_drift: f32,
lang: Option<&str>,
limit: Option<usize>,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_drift", reference).entered();
let threshold = validate_finite_f32(threshold, "threshold")?;
let min_drift = validate_finite_f32(min_drift, "min_drift")?;
ctx.get_ref(reference)?;
let ref_idx = ctx
.borrow_ref(reference)
.ok_or_else(|| anyhow::anyhow!("Reference '{}' not loaded", reference))?;
let result = cqs::drift::detect_drift(
&ref_idx.store,
&ctx.store(),
reference,
threshold,
min_drift,
lang,
)?;
let mut drifted_json: Vec<_> = result
.drifted
.iter()
.map(|e| {
serde_json::json!({
"name": e.name,
"file": e.file.display().to_string(),
"chunk_type": e.chunk_type,
"similarity": e.similarity,
"drift": e.drift,
})
})
.collect();
if let Some(lim) = limit {
drifted_json.truncate(lim);
}
Ok(serde_json::json!({
"reference": result.reference,
"threshold": result.threshold,
"min_drift": result.min_drift,
"drifted": drifted_json,
"total_compared": result.total_compared,
"unchanged": result.unchanged,
}))
}
pub(in crate::cli::batch) fn dispatch_diff(
ctx: &BatchContext,
source: &str,
target: Option<&str>,
threshold: f32,
lang: Option<&str>,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_diff", source).entered();
let threshold = validate_finite_f32(threshold, "threshold")?;
let source_store = crate::cli::commands::resolve::resolve_reference_store(&ctx.root, source)?;
let target_label = target.unwrap_or("project");
let target_store = if target_label == "project" {
&ctx.store()
} else {
ctx.get_ref(target_label)?;
&ctx.store() };
let result = if target_label == "project" {
cqs::semantic_diff(
&source_store,
target_store,
source,
target_label,
threshold,
lang,
)?
} else {
let target_ref_store =
crate::cli::commands::resolve::resolve_reference_store(&ctx.root, target_label)?;
cqs::semantic_diff(
&source_store,
&target_ref_store,
source,
target_label,
threshold,
lang,
)?
};
let added: Vec<_> = result
.added
.iter()
.map(|e| {
serde_json::json!({
"name": e.name,
"file": e.file.display().to_string(),
"type": e.chunk_type.to_string(),
})
})
.collect();
let removed: Vec<_> = result
.removed
.iter()
.map(|e| {
serde_json::json!({
"name": e.name,
"file": e.file.display().to_string(),
"type": e.chunk_type.to_string(),
})
})
.collect();
let modified: Vec<_> = result
.modified
.iter()
.map(|e| {
serde_json::json!({
"name": e.name,
"file": e.file.display().to_string(),
"type": e.chunk_type.to_string(),
"similarity": e.similarity,
})
})
.collect();
Ok(serde_json::json!({
"source": result.source,
"target": result.target,
"added": added,
"removed": removed,
"modified": modified,
"summary": {
"added": result.added.len(),
"removed": result.removed.len(),
"modified": result.modified.len(),
"unchanged": result.unchanged_count,
}
}))
}
pub(in crate::cli::batch) fn dispatch_plan(
ctx: &BatchContext,
description: &str,
limit: usize,
tokens: Option<usize>,
) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_plan", description).entered();
let embedder = ctx.embedder()?;
let result = cqs::plan::plan(&ctx.store(), embedder, description, &ctx.root, limit)
.context("Plan generation failed")?;
let mut json = serde_json::to_value(&result)?;
if let Some(budget) = tokens {
json["token_budget"] = serde_json::json!(budget);
}
Ok(json)
}
pub(in crate::cli::batch) fn dispatch_gc(ctx: &BatchContext) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_gc").entered();
let file_set = ctx.file_set()?;
let (stale_count, missing_count) = match ctx.store().count_stale_files(&file_set) {
Ok(counts) => counts,
Err(e) => {
tracing::warn!(error = %e, "Failed to count stale files");
(0, 0)
}
};
let prune = ctx
.store()
.prune_all(&file_set)
.context("Failed to prune stale entries from index")?;
let output = crate::cli::commands::GcOutput {
stale_files: stale_count as usize,
missing_files: missing_count as usize,
pruned_chunks: prune.pruned_chunks as usize,
pruned_calls: prune.pruned_calls as usize,
pruned_type_edges: prune.pruned_type_edges as usize,
pruned_summaries: prune.pruned_summaries,
hnsw_rebuilt: false,
hnsw_vectors: None,
};
Ok(serde_json::to_value(&output)?)
}
pub(in crate::cli::batch) fn dispatch_refresh(ctx: &BatchContext) -> Result<serde_json::Value> {
let _span = tracing::info_span!("batch_refresh").entered();
ctx.invalidate()?;
Ok(serde_json::json!({"status": "ok", "message": "Caches invalidated, Store re-opened"}))
}
pub(in crate::cli::batch) fn dispatch_help() -> Result<serde_json::Value> {
use clap::CommandFactory;
let mut buf = Vec::new();
BatchInput::command().write_help(&mut buf)?;
let help_text = String::from_utf8_lossy(&buf).to_string();
Ok(serde_json::json!({"help": help_text}))
}