use anyhow::{Context, Result};
use super::super::commands::BatchInput;
use super::super::BatchContext;
use crate::cli::args::GatherArgs;
use crate::cli::validate_finite_f32;
pub(in crate::cli::batch) fn dispatch_gather(
ctx: &BatchContext,
args: &GatherArgs,
) -> Result<serde_json::Value> {
let query = args.query.as_str();
let ref_name = args.ref_name.as_deref();
let _span = tracing::info_span!("batch_gather", query, ?ref_name).entered();
let embedder = ctx.embedder()?;
let opts = cqs::GatherOptions {
expand_depth: args.expand.clamp(0, 5),
direction: args.direction,
limit: args.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) = args.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, crate::cli::SCOUT_LIMIT_MAX);
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();
anyhow::bail!(
"gc requires a writable store; run `cqs gc` outside the daemon. \
(Commands::Gc is BatchSupport::Cli in dispatch.rs; reaching this \
branch means a daemon classifier regressed — see #946.)"
)
}
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}))
}