use aptu_coder_core::analyze;
use aptu_coder_core::cache::CacheTier;
use aptu_coder_core::formatter::format_module_info;
use aptu_coder_core::types::{AnalyzeModuleParams, ModuleInfo};
use rmcp::model::{CallToolResult, ContentBlock, ErrorData, MetaObject};
use std::sync::Arc;
use tracing::instrument;
use crate::tools::common::{err_to_tool_result, error_meta, no_cache_meta};
pub(crate) struct AnalyzeModuleContext {
pub(crate) disk_cache: Arc<aptu_coder_core::cache::DiskCache>,
pub(crate) metrics_tx: crate::metrics::MetricsSender,
pub(crate) sid: Option<String>,
pub(crate) seq: u32,
}
#[instrument(skip(ctx, params, span))]
pub(crate) async fn analyze_module_handler(
ctx: AnalyzeModuleContext,
params: AnalyzeModuleParams,
param_path: String,
span: &tracing::Span,
t_start: std::time::Instant,
) -> Result<CallToolResult, ErrorData> {
let sid = ctx.sid.clone();
let seq = ctx.seq;
if std::fs::metadata(¶ms.path)
.map(|m| m.is_dir())
.unwrap_or(false)
{
span.record("error", true);
span.record("error.type", "invalid_params");
let dur = t_start.elapsed().as_millis().try_into().unwrap_or(u64::MAX);
ctx.metrics_tx.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "error", dur)
.param_path_depth(crate::metrics::path_component_count(¶m_path))
.error_type(Some("invalid_params".to_string()))
.session_id(sid.clone())
.seq(Some(seq))
.build(),
);
return Ok(err_to_tool_result(ErrorData::new(
rmcp::model::ErrorCode::INVALID_PARAMS,
"path is a directory; use analyze_directory for directories, or pass a file path to analyze_module",
{
let mut meta =
error_meta("validation", false, "use analyze_directory for directories");
if let Some(obj) = meta.as_object_mut() {
obj.insert("path".to_string(), serde_json::json!(params.path));
}
Some(meta)
},
)));
}
let file_bytes = match tokio::fs::read(¶ms.path).await {
Ok(b) => b,
Err(_e) => {
let dur = t_start.elapsed().as_millis().try_into().unwrap_or(u64::MAX);
ctx.metrics_tx.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "error", dur)
.param_path_depth(crate::metrics::path_component_count(¶m_path))
.error_type(Some("internal_error".to_string()))
.session_id(sid.clone())
.seq(Some(seq))
.file_ext(crate::metrics::path_file_ext(¶m_path))
.language(crate::metrics::path_language(¶m_path))
.build(),
);
return Ok(err_to_tool_result(ErrorData::new(
rmcp::model::ErrorCode::INTERNAL_ERROR,
"failed to read file; check file path and permissions",
{
let mut meta = error_meta("resource", false, "check file path and permissions");
if let Some(obj) = meta.as_object_mut() {
obj.insert("path".to_string(), serde_json::json!(params.path));
}
Some(meta)
},
)));
}
};
let disk_key = blake3::hash(&file_bytes);
let (module_info, module_tier) = if let Some(cached) = ctx
.disk_cache
.get::<ModuleInfo>("analyze_module", &disk_key)
{
(cached, CacheTier::L2Disk)
} else {
let mi = match analyze::analyze_module_file(¶ms.path) {
Ok(mi) => mi,
Err(e) => {
let dur = t_start.elapsed().as_millis().try_into().unwrap_or(u64::MAX);
if matches!(
&e,
analyze::AnalyzeError::Parser(
aptu_coder_core::parser::ParserError::UnsupportedLanguage(_)
)
) {
let source = String::from_utf8_lossy(&file_bytes).into_owned();
let line_count = source.lines().count();
let name = std::path::Path::new(¶ms.path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let ext = std::path::Path::new(¶ms.path)
.extension()
.and_then(|x| x.to_str())
.unwrap_or("unknown")
.to_string();
ctx.metrics_tx.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "ok", dur)
.param_path_depth(crate::metrics::path_component_count(¶m_path))
.session_id(sid.clone())
.seq(Some(seq))
.file_ext(crate::metrics::path_file_ext(¶m_path))
.language(crate::metrics::path_language(¶m_path))
.build(),
);
return {
let mut mi = ModuleInfo::new(name, line_count, ext, vec![], vec![]);
mi.unsupported = Some(true);
let text = format_module_info(&mi);
let content_hash = format!("{}", blake3::hash(text.as_bytes()));
let mut meta = no_cache_meta().0;
meta.insert(
"content_hash".to_string(),
serde_json::Value::String(content_hash),
);
let mut result = CallToolResult::success(vec![ContentBlock::text(text)])
.with_meta(Some(MetaObject(meta)));
match serde_json::to_value(&mi) {
Ok(v) => {
result.structured_content = Some(v);
Ok(result)
}
Err(se) => Ok(err_to_tool_result(ErrorData::new(
rmcp::model::ErrorCode::INTERNAL_ERROR,
format!("serialization failed: {se}"),
Some(error_meta("internal", false, "report this as a bug")),
))),
}
};
}
let (error_type, error_data) = (
Some("internal_error".to_string()),
ErrorData::new(
rmcp::model::ErrorCode::INTERNAL_ERROR,
format!("Failed to analyze module: {e}"),
Some(error_meta("internal", false, "report this as a bug")),
),
);
ctx.metrics_tx.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "error", dur)
.param_path_depth(crate::metrics::path_component_count(¶m_path))
.error_type(error_type)
.session_id(sid.clone())
.seq(Some(seq))
.file_ext(crate::metrics::path_file_ext(¶m_path))
.language(crate::metrics::path_language(¶m_path))
.build(),
);
return Ok(err_to_tool_result(error_data));
}
};
{
let dc = ctx.disk_cache.clone();
let k = disk_key;
let mi_clone = mi.clone();
let metrics_tx2 = ctx.metrics_tx.clone();
let sid2 = sid.clone();
tokio::spawn(async move {
let handle = tokio::task::spawn_blocking(move || {
dc.put("analyze_module", &k, &mi_clone);
dc.drain_write_failures()
});
if let Ok(failures) = handle.await
&& failures > 0
{
tracing::warn!(
tool = "analyze_module",
failures,
"L2 disk cache write failed"
);
metrics_tx2.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "ok", 0)
.session_id(sid2)
.cache_write_failure(Some(true))
.build(),
);
}
});
}
(mi, CacheTier::Miss)
};
let text = format_module_info(&module_info);
tracing::Span::current().record("cache_tier", module_tier.as_str());
let content_hash = format!("{}", blake3::hash(text.as_bytes()));
let mut meta = no_cache_meta().0;
meta.insert(
"content_hash".to_string(),
serde_json::Value::String(content_hash),
);
let mut result = CallToolResult::success(vec![ContentBlock::text(text.clone())])
.with_meta(Some(MetaObject(meta)));
let structured = match serde_json::to_value(&module_info).map_err(|e| {
ErrorData::new(
rmcp::model::ErrorCode::INTERNAL_ERROR,
format!("serialization failed: {e}"),
Some(error_meta("internal", false, "report this as a bug")),
)
}) {
Ok(v) => v,
Err(e) => return Ok(err_to_tool_result(e)),
};
result.structured_content = Some(structured);
let dur = t_start.elapsed().as_millis().try_into().unwrap_or(u64::MAX);
ctx.metrics_tx.send(
crate::metrics::MetricEventBuilder::new("analyze_module", "ok", dur)
.output_chars(text.len())
.param_path_depth(crate::metrics::path_component_count(¶m_path))
.session_id(sid)
.seq(Some(seq))
.cache_hit(Some(module_tier != CacheTier::Miss))
.cache_tier(Some(module_tier.as_str()))
.file_ext(crate::metrics::path_file_ext(¶m_path))
.language(crate::metrics::path_language(¶m_path))
.build(),
);
Ok(result)
}