use anyhow::{bail, Context, Result};
use magellan::OutputFormat;
use std::path::PathBuf;
use crate::service::registry::Registry;
#[derive(Debug, PartialEq)]
pub enum Intent {
Callers,
Callees,
Cfg,
BlastZone,
Cycles,
Impact,
Complex,
Search,
Find,
}
pub fn detect_intent(q: &str) -> Intent {
if [
"who calls",
"who uses",
"callers of",
"who references",
"who invokes",
"dependencies of",
"dependents of",
"who depends on",
]
.iter()
.any(|p| q.contains(p))
{
return Intent::Callers;
}
if [
"who is called by",
"callees of",
"calls from",
"outgoing calls",
"called by",
]
.iter()
.any(|p| q.contains(p))
{
return Intent::Callees;
}
if ["cfg for", "control flow", "cfg of", "show cfg"]
.iter()
.any(|p| q.contains(p))
{
return Intent::Cfg;
}
if [
"blast zone",
"blast-zone",
"hot paths",
"hotpaths",
"hot path",
]
.iter()
.any(|p| q.contains(p))
{
return Intent::BlastZone;
}
if ["cycles", "circular", "cyclic", "strongly connected"]
.iter()
.any(|p| q.contains(p))
{
return Intent::Cycles;
}
if ["impact of", "affected by", "what breaks", "what changes"]
.iter()
.any(|p| q.contains(p))
{
return Intent::Impact;
}
if ["complex", "high complexity", "most complex", "complicated"]
.iter()
.any(|p| q.contains(p))
{
return Intent::Complex;
}
if ["search", "semantic", "find code", "look for"]
.iter()
.any(|p| q.contains(p))
{
return Intent::Search;
}
Intent::Find
}
pub fn run_ask(
question: String,
db_path: PathBuf,
all: bool,
output_format: OutputFormat,
) -> Result<()> {
if all {
return run_ask_all(question, output_format);
}
let q = question.to_lowercase();
let intent = detect_intent(&q);
match intent {
Intent::Cycles => return route_cycles(db_path, output_format),
Intent::Search => return route_search(db_path, &question, output_format),
Intent::Complex => return route_complex(db_path, &question, output_format),
_ => {}
}
let name = extract_quoted_symbol(&q)
.or_else(|| q.split_whitespace().last().map(str::to_string))
.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine symbol name from question. Try: \"who calls 'run_find'\""
)
})?;
match intent {
Intent::Callers => route_refs(db_path, name, "in".to_string(), output_format),
Intent::Callees => route_refs(db_path, name, "out".to_string(), output_format),
Intent::Cfg => route_cfg(db_path, name, output_format),
Intent::BlastZone => route_blast_zone(db_path, name, output_format),
Intent::Impact => route_impact(db_path, name, output_format),
Intent::Find => route_find(db_path, name, output_format),
Intent::Cycles | Intent::Search | Intent::Complex => unreachable!(),
}
}
fn run_ask_all(question: String, output_format: OutputFormat) -> Result<()> {
let registry = Registry::load()?;
let enabled: Vec<_> = registry.projects.iter().filter(|p| p.enabled).collect();
if enabled.is_empty() {
println!("No enabled projects in registry.");
println!("Hint: use `magellan registry scan` to discover projects.");
return Ok(());
}
for entry in &enabled {
println!("=== {} ===", entry.name);
if entry.db.exists() {
if let Err(e) = run_ask(question.clone(), entry.db.clone(), false, output_format) {
println!(" error: {}", e);
}
} else {
println!(" database not found: {}", entry.db.display());
}
println!();
}
Ok(())
}
fn extract_quoted_symbol(q: &str) -> Option<String> {
for (open, close) in [('\'', '\''), ('\"', '\"')] {
if let Some(start) = q.find(open) {
let rest = &q[start + 1..];
if let Some(end) = rest.find(close) {
let candidate = &rest[..end];
if !candidate.is_empty() {
return Some(candidate.to_string());
}
}
}
}
None
}
fn route_find(db_path: PathBuf, name: String, output_format: OutputFormat) -> Result<()> {
crate::find_cmd::run_find(
db_path,
Some(name),
None, None, None, None, None, false, output_format,
true, true, true, true, false, 3, false, )
.with_context(|| "Ask → find routing failed")
}
fn route_refs(
db_path: PathBuf,
name: String,
direction: String,
output_format: OutputFormat,
) -> Result<()> {
crate::refs_cmd::run_refs(
db_path,
name,
None, None, None, direction,
output_format,
true, true, false, 3, false, )
.with_context(|| "Ask → refs routing failed")
}
fn route_cfg(db_path: PathBuf, name: String, _output_format: OutputFormat) -> Result<()> {
let db = db_path.to_string_lossy();
let status = std::process::Command::new("mirage")
.args(["cfg", "--db", &db, "--function", &name])
.status()
.with_context(|| "Ask → cfg: failed to run mirage")?;
if !status.success() {
bail!("mirage exited with {}", status);
}
Ok(())
}
fn route_blast_zone(db_path: PathBuf, name: String, _output_format: OutputFormat) -> Result<()> {
let db = db_path.to_string_lossy();
let status = std::process::Command::new("mirage")
.args(["blast-zone", "--db", &db, "--function", &name])
.status()
.with_context(|| "Ask → blast-zone: failed to run mirage")?;
if !status.success() {
bail!("mirage exited with {}", status);
}
Ok(())
}
fn route_cycles(db_path: PathBuf, output_format: OutputFormat) -> Result<()> {
crate::cycles_cmd::run_cycles(db_path, None, output_format)
.with_context(|| "Ask → cycles routing failed")
}
fn route_impact(db_path: PathBuf, name: String, output_format: OutputFormat) -> Result<()> {
crate::context_cmd::run_context_impact(
vec![db_path],
name,
None, 3, None, output_format,
)
.with_context(|| "Ask → impact routing failed")
}
fn route_complex(db_path: PathBuf, question: &str, _output_format: OutputFormat) -> Result<()> {
let db = db_path.to_string_lossy();
let status = std::process::Command::new("llmgrep")
.args([
"--db",
&db,
"search",
"--query",
question,
"--min-complexity",
"10",
])
.status()
.with_context(|| "Ask → complex: failed to run llmgrep")?;
if !status.success() {
bail!("llmgrep exited with {}", status);
}
Ok(())
}
fn route_search(db_path: PathBuf, question: &str, _output_format: OutputFormat) -> Result<()> {
let db = db_path.to_string_lossy();
let status = std::process::Command::new("llmgrep")
.args(["--db", &db, "search", "--query", question])
.status()
.with_context(|| "Ask → search: failed to run llmgrep")?;
if !status.success() {
bail!("llmgrep exited with {}", status);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_intent_callers() {
assert_eq!(detect_intent("who calls run_find"), Intent::Callers);
assert_eq!(detect_intent("callers of parse_args"), Intent::Callers);
assert_eq!(detect_intent("who references foo"), Intent::Callers);
assert_eq!(detect_intent("who depends on bar"), Intent::Callers);
}
#[test]
fn test_detect_intent_callees() {
assert_eq!(detect_intent("callees of main"), Intent::Callees);
assert_eq!(
detect_intent("outgoing calls from run_watch"),
Intent::Callees
);
}
#[test]
fn test_detect_intent_cfg() {
assert_eq!(detect_intent("cfg for run_status"), Intent::Cfg);
assert_eq!(
detect_intent("control flow of parse_watch_args"),
Intent::Cfg
);
assert_eq!(detect_intent("show cfg of main"), Intent::Cfg);
}
#[test]
fn test_detect_intent_blast_zone() {
assert_eq!(
detect_intent("blast zone of handle_request"),
Intent::BlastZone
);
assert_eq!(detect_intent("hot paths in run_find"), Intent::BlastZone);
assert_eq!(detect_intent("blast-zone of foo"), Intent::BlastZone);
}
#[test]
fn test_detect_intent_cycles() {
assert_eq!(detect_intent("cycles in the call graph"), Intent::Cycles);
assert_eq!(detect_intent("circular dependencies"), Intent::Cycles);
assert_eq!(
detect_intent("strongly connected components"),
Intent::Cycles
);
}
#[test]
fn test_detect_intent_impact() {
assert_eq!(detect_intent("impact of resolve_db_path"), Intent::Impact);
assert_eq!(detect_intent("affected by run_status"), Intent::Impact);
assert_eq!(detect_intent("what breaks if i change foo"), Intent::Impact);
}
#[test]
fn test_detect_intent_complex() {
assert_eq!(detect_intent("complex functions in src"), Intent::Complex);
assert_eq!(detect_intent("high complexity code"), Intent::Complex);
assert_eq!(detect_intent("most complex function"), Intent::Complex);
}
#[test]
fn test_detect_intent_search() {
assert_eq!(detect_intent("search for error handling"), Intent::Search);
assert_eq!(
detect_intent("semantic query about retry logic"),
Intent::Search
);
assert_eq!(detect_intent("find code that parses toml"), Intent::Search);
}
#[test]
fn test_detect_intent_find_fallback() {
assert_eq!(detect_intent("run_status"), Intent::Find);
assert_eq!(detect_intent("where is parse_watch_args"), Intent::Find);
}
#[test]
fn test_extract_quoted_symbol_single_quotes() {
assert_eq!(
extract_quoted_symbol("who calls 'run_find'"),
Some("run_find".to_string())
);
}
#[test]
fn test_extract_quoted_symbol_double_quotes() {
assert_eq!(
extract_quoted_symbol("cfg for \"parse_status_args\""),
Some("parse_status_args".to_string())
);
}
#[test]
fn test_extract_quoted_symbol_none() {
assert_eq!(extract_quoted_symbol("who calls run_find"), None);
}
}