use crate::error::LlmError;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolSet {
pub symbol_ids: Vec<String>,
}
impl SymbolSet {
pub fn from_file(path: &Path) -> Result<Self, LlmError> {
let content = std::fs::read_to_string(path).map_err(LlmError::IoError)?;
serde_json::from_str(&content).map_err(LlmError::JsonError)
}
pub fn validate(&self) -> Result<(), LlmError> {
for symbol_id in &self.symbol_ids {
if symbol_id.len() != 32 {
return Err(LlmError::InvalidQuery {
query: format!(
"Invalid SymbolId format: '{}'. Expected 32 hex characters.",
symbol_id
),
});
}
if !symbol_id.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(LlmError::InvalidQuery {
query: format!(
"Invalid SymbolId format: '{}'. Expected only hexadecimal characters.",
symbol_id
),
});
}
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.symbol_ids.is_empty()
}
pub fn len(&self) -> usize {
self.symbol_ids.len()
}
}
#[derive(Debug, Clone, Default)]
pub struct AlgorithmOptions<'a> {
pub from_symbol_set: Option<&'a str>,
pub reachable_from: Option<&'a str>,
pub dead_code_in: Option<&'a str>,
pub in_cycle: Option<&'a str>,
pub slice_backward_from: Option<&'a str>,
pub slice_forward_from: Option<&'a str>,
pub condense: bool,
pub paths_from: Option<&'a str>,
pub paths_to: Option<&'a str>,
}
impl<'a> AlgorithmOptions<'a> {
pub fn is_active(&self) -> bool {
self.from_symbol_set.is_some()
|| self.reachable_from.is_some()
|| self.dead_code_in.is_some()
|| self.in_cycle.is_some()
|| self.slice_backward_from.is_some()
|| self.slice_forward_from.is_some()
|| self.condense
|| self.paths_from.is_some()
}
pub fn new() -> Self {
Self::default()
}
}
pub type AlgorithmFilterResult = Result<(Vec<String>, HashMap<String, String>, bool), LlmError>;
pub fn apply_algorithm_filters(
db_path: &Path,
options: &AlgorithmOptions<'_>,
) -> AlgorithmFilterResult {
if let Some(file_path) = options.from_symbol_set {
let symbol_set = magellan_bridge::parse_symbol_set_file(Path::new(file_path))?;
symbol_set.validate()?;
return Ok((symbol_set.symbol_ids, HashMap::new(), false));
}
let active_count = [
options.reachable_from.is_some(),
options.dead_code_in.is_some(),
options.in_cycle.is_some(),
options.slice_backward_from.is_some(),
options.slice_forward_from.is_some(),
options.condense,
options.paths_from.is_some(),
]
.iter()
.filter(|&&x| x)
.count();
if active_count > 1 {
return Err(LlmError::InvalidQuery {
query: "Only one one-shot algorithm filter may be specified. Use --from-symbol-set for composed filters.".to_string(),
});
}
if let Some(symbol) = options.reachable_from {
let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
let args = ["--from", &symbol_id];
return Ok((
magellan_bridge::run_magellan_algorithm(db_path, "reachable", &args)?.symbol_ids,
HashMap::new(),
false,
));
}
if let Some(symbol) = options.dead_code_in {
let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
let args = ["--entry", &symbol_id];
return Ok((
magellan_bridge::run_magellan_algorithm(db_path, "dead-code", &args)?.symbol_ids,
HashMap::new(),
false,
));
}
if let Some(symbol) = options.in_cycle {
let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
let args = ["--symbol", &symbol_id];
return Ok((
magellan_bridge::run_magellan_algorithm(db_path, "cycles", &args)?.symbol_ids,
HashMap::new(),
false,
));
}
if let Some(symbol) = options.slice_backward_from {
let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
let args = ["--target", &symbol_id, "--direction", "backward"];
return Ok((
magellan_bridge::run_magellan_algorithm(db_path, "slice", &args)?.symbol_ids,
HashMap::new(),
false,
));
}
if let Some(symbol) = options.slice_forward_from {
let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
let args = ["--target", &symbol_id, "--direction", "forward"];
return Ok((
magellan_bridge::run_magellan_algorithm(db_path, "slice", &args)?.symbol_ids,
HashMap::new(),
false,
));
}
if options.condense {
let output = Command::new("magellan")
.args([
"condense",
"--db",
&db_path.to_string_lossy(),
"--output",
"json",
])
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
_ => LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: format!(
"{}\n\nTry running: magellan condense --db {} for more details",
e,
db_path.to_string_lossy()
),
},
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: format!(
"{}\n\nTry running: magellan condense --db {} for more details",
stderr,
db_path.to_string_lossy()
),
});
}
let json = String::from_utf8_lossy(&output.stdout);
let (symbol_ids, supernode_map) = magellan_bridge::parse_condense_output(&json)?;
return Ok((symbol_ids, supernode_map, false));
}
if let Some(start_symbol) = options.paths_from {
let start_id = resolve_fqn_to_symbol_id(db_path, start_symbol)?;
let db_path_str = db_path.to_string_lossy().to_string();
let mut args = vec![
"paths".to_string(),
"--db".to_string(),
db_path_str.clone(),
"--start".to_string(),
start_id.clone(),
"--max-depth".to_string(),
"100".to_string(),
"--max-paths".to_string(),
"1000".to_string(),
"--output".to_string(),
"json".to_string(),
];
if let Some(end_symbol) = options.paths_to {
let end_id = resolve_fqn_to_symbol_id(db_path, end_symbol)?;
args.push("--end".to_string());
args.push(end_id);
}
let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = Command::new("magellan")
.args(&args_refs)
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
_ => LlmError::MagellanExecutionFailed {
algorithm: "paths".to_string(),
stderr: format!(
"{}\n\nTry running: magellan paths --db {} --start {} for more details",
e, db_path_str, start_id
),
},
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(LlmError::MagellanExecutionFailed {
algorithm: "paths".to_string(),
stderr: format!(
"{}\n\nTry running: magellan paths --db {} --start {} for more details",
stderr, db_path_str, start_id
),
});
}
let json = String::from_utf8_lossy(&output.stdout);
let (symbol_ids, bounded_hit) = magellan_bridge::parse_paths_output(&json)?;
return Ok((symbol_ids, HashMap::new(), bounded_hit));
}
Ok((Vec::new(), HashMap::new(), false))
}
const SYMBOL_SET_TEMP_TABLE_THRESHOLD: usize = 1000;
pub fn create_symbol_set_temp_table(
conn: &Connection,
symbol_ids: &[String],
) -> Result<String, LlmError> {
let table_name = format!("symbol_set_filter_{}", std::process::id());
conn.execute(
&format!(
"CREATE TEMP TABLE {} (symbol_id TEXT PRIMARY KEY)",
table_name
),
[],
)
.map_err(LlmError::SqliteError)?;
let mut stmt = conn
.prepare(&format!(
"INSERT INTO {} (symbol_id) VALUES (?)",
table_name
))
.map_err(LlmError::SqliteError)?;
for symbol_id in symbol_ids {
stmt.execute([symbol_id]).map_err(LlmError::SqliteError)?;
}
Ok(table_name)
}
pub fn symbol_set_filter_strategy(symbol_ids: &[String]) -> SymbolSetStrategy {
if symbol_ids.is_empty() {
SymbolSetStrategy::None
} else if symbol_ids.len() > SYMBOL_SET_TEMP_TABLE_THRESHOLD {
SymbolSetStrategy::TempTable
} else {
SymbolSetStrategy::InClause
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SymbolSetStrategy {
None,
InClause,
TempTable,
}
pub fn resolve_fqn_to_symbol_id(db_path: &Path, name: &str) -> Result<String, LlmError> {
magellan_bridge::check_magellan_available()?;
let output = Command::new("magellan")
.args([
"find",
"--db",
&db_path.to_string_lossy(),
"--name",
name,
"--output",
"json",
])
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
_ => LlmError::SearchFailed {
reason: format!("Failed to execute magellan find: {}", e),
},
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(LlmError::SearchFailed {
reason: format!("magellan find failed: {}", stderr),
});
}
let json: Value = serde_json::from_slice(&output.stdout).map_err(LlmError::JsonError)?;
let matches = json
.get("data")
.and_then(|d| d.get("matches"))
.and_then(|m| m.as_array())
.ok_or_else(|| LlmError::InvalidQuery {
query: "Invalid magellan find output: missing 'data.matches' array".to_string(),
})?;
if matches.len() > 1 {
return Err(LlmError::AmbiguousSymbolName {
name: name.to_string(),
count: matches.len(),
});
}
if matches.is_empty() {
return Err(LlmError::InvalidQuery {
query: format!("Symbol '{}' not found in database", name),
});
}
matches[0]["symbol_id"]
.as_str()
.map(|id| id.to_string())
.ok_or_else(|| LlmError::InvalidQuery {
query: format!(
"Symbol '{}' found but missing symbol_id in magellan output",
name
),
})
}
pub use magellan_bridge::{
check_magellan_available, parse_condense_output, parse_paths_output, parse_symbol_set_file,
run_magellan_algorithm,
};
mod magellan_bridge;
#[cfg(test)]
mod tests;