use crate::error::LlmError;
use serde_json::Value;
use std::path::Path;
use std::process::Command;
use super::SymbolSet;
pub fn check_magellan_available() -> Result<(), LlmError> {
check_magellan_version()
}
fn check_magellan_version() -> Result<(), LlmError> {
let output = Command::new("magellan")
.arg("--version")
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
_ => LlmError::MagellanExecutionFailed {
algorithm: "version check".to_string(),
stderr: e.to_string(),
},
})?;
if !output.status.success() {
return Err(LlmError::MagellanExecutionFailed {
algorithm: "version check".to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
let version_str = String::from_utf8_lossy(&output.stdout);
let version = parse_magellan_version(&version_str)?;
if version < (2, 1, 0) {
return Err(LlmError::MagellanVersionMismatch {
current: format!("{}.{}.{}", version.0, version.1, version.2),
required: "2.1.0".to_string(),
});
}
Ok(())
}
fn parse_magellan_version(output: &str) -> Result<(u32, u32, u32), LlmError> {
let first_line = output.lines().next().unwrap_or("");
let version_part = first_line
.strip_prefix("magellan")
.unwrap_or("")
.split_whitespace()
.next()
.unwrap_or("");
let parts: Vec<&str> = version_part.split('.').collect();
if parts.len() != 3 {
return Err(LlmError::MagellanExecutionFailed {
algorithm: "version parsing".to_string(),
stderr: format!("Unable to parse version from: {}", first_line),
});
}
let major: u32 = parts[0]
.parse()
.map_err(|_| LlmError::MagellanExecutionFailed {
algorithm: "version parsing".to_string(),
stderr: format!("Invalid major version: {}", parts[0]),
})?;
let minor: u32 = parts[1]
.parse()
.map_err(|_| LlmError::MagellanExecutionFailed {
algorithm: "version parsing".to_string(),
stderr: format!("Invalid minor version: {}", parts[1]),
})?;
let patch: u32 = parts[2]
.parse()
.map_err(|_| LlmError::MagellanExecutionFailed {
algorithm: "version parsing".to_string(),
stderr: format!("Invalid patch version: {}", parts[2]),
})?;
Ok((major, minor, patch))
}
pub fn run_magellan_algorithm(
db_path: &Path,
algorithm: &str,
args: &[&str],
) -> Result<SymbolSet, LlmError> {
check_magellan_available()?;
let mut cmd = Command::new("magellan");
cmd.arg(algorithm)
.arg("--db")
.arg(db_path)
.arg("--output")
.arg("json")
.args(args);
let output = cmd.output().map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
_ => LlmError::MagellanExecutionFailed {
algorithm: algorithm.to_string(),
stderr: e.to_string(),
},
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(LlmError::MagellanExecutionFailed {
algorithm: algorithm.to_string(),
stderr: stderr.to_string(),
});
}
let json_str = String::from_utf8_lossy(&output.stdout);
extract_symbol_ids_from_magellan_json(&json_str, algorithm)
}
fn extract_symbol_ids_from_magellan_json(
json: &str,
algorithm: &str,
) -> Result<SymbolSet, LlmError> {
let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
let symbol_ids = match algorithm {
"reachable" => {
let symbols =
parsed["result"]["symbols"]
.as_array()
.ok_or_else(|| LlmError::InvalidQuery {
query: "Missing 'symbols' array in reachable output".to_string(),
})?;
symbols
.iter()
.filter_map(|s| s["symbol_id"].as_str().map(|id| id.to_string()))
.collect()
}
"dead-code" => {
let dead_symbols = parsed["result"]["dead_symbols"].as_array().ok_or_else(|| {
LlmError::InvalidQuery {
query: "Missing 'dead_symbols' array in dead-code output".to_string(),
}
})?;
dead_symbols
.iter()
.filter_map(|s| s["symbol"]["symbol_id"].as_str().map(|id| id.to_string()))
.collect()
}
"slice" => {
let included_symbols =
parsed["result"]["included_symbols"]
.as_array()
.ok_or_else(|| LlmError::InvalidQuery {
query: "Missing 'included_symbols' array in slice output".to_string(),
})?;
included_symbols
.iter()
.filter_map(|s| s["symbol_id"].as_str().map(|id| id.to_string()))
.collect()
}
"cycles" => {
let cycles =
parsed["result"]["cycles"]
.as_array()
.ok_or_else(|| LlmError::InvalidQuery {
query: "Missing 'cycles' array in cycles output".to_string(),
})?;
let mut ids = Vec::new();
for cycle in cycles {
if let Some(members) = cycle["members"].as_array() {
for member in members {
if let Some(id) = member["symbol_id"].as_str() {
ids.push(id.to_string());
}
}
}
}
ids
}
_ => {
return Err(LlmError::InvalidQuery {
query: format!("Unknown algorithm: {}", algorithm),
});
}
};
Ok(SymbolSet { symbol_ids })
}
pub fn parse_condense_output(
json: &str,
) -> Result<(Vec<String>, std::collections::HashMap<String, String>), LlmError> {
use std::collections::HashMap;
let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
let supernodes = if parsed["data"]["supernodes"].is_array() {
parsed["data"]["supernodes"].as_array()
} else {
parsed["supernodes"].as_array()
}
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: "Missing 'supernodes' array in condense output".to_string(),
})?;
let mut all_symbol_ids = Vec::new();
let mut supernode_map = HashMap::new();
for supernode in supernodes {
let supernode_id = if let Some(id_str) = supernode["id"].as_str() {
id_str.to_string()
} else if let Some(id_num) = supernode["id"].as_u64() {
format!("supernode_{}", id_num)
} else if let Some(id_i64) = supernode["id"].as_i64() {
format!("supernode_{}", id_i64)
} else {
return Err(LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: "Supernode missing 'id' field".to_string(),
});
};
let members =
supernode["members"]
.as_array()
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: "Supernode missing 'members' array".to_string(),
})?;
for member in members {
let symbol_id =
member["symbol_id"]
.as_str()
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "condense".to_string(),
stderr: "Member missing 'symbol_id' field".to_string(),
})?;
all_symbol_ids.push(symbol_id.to_string());
supernode_map.insert(symbol_id.to_string(), supernode_id.to_string());
}
}
Ok((all_symbol_ids, supernode_map))
}
pub fn parse_paths_output(json: &str) -> Result<(Vec<String>, bool), LlmError> {
use std::collections::HashSet;
let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
let paths = if parsed["data"]["paths"].is_array() {
parsed["data"]["paths"].as_array()
} else {
parsed["paths"].as_array()
}
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "paths".to_string(),
stderr: "Missing 'paths' array in output".to_string(),
})?;
let mut all_symbol_ids = HashSet::new();
for path in paths {
let symbols =
path["symbols"]
.as_array()
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "paths".to_string(),
stderr: "Path missing 'symbols' array".to_string(),
})?;
for symbol in symbols {
let symbol_id =
symbol["symbol_id"]
.as_str()
.ok_or_else(|| LlmError::MagellanExecutionFailed {
algorithm: "paths".to_string(),
stderr: "Symbol missing 'symbol_id' field".to_string(),
})?;
all_symbol_ids.insert(symbol_id.to_string());
}
}
let bounded_hit = parsed["bounded_hit"]
.as_bool()
.or_else(|| parsed["data"]["bounded_hit"].as_bool())
.unwrap_or(false);
Ok((all_symbol_ids.into_iter().collect(), bounded_hit))
}
pub fn parse_symbol_set_file(path: &Path) -> Result<SymbolSet, LlmError> {
let symbol_set = SymbolSet::from_file(path)?;
symbol_set.validate()?;
Ok(symbol_set)
}