use pathfinder_common::error::PathfinderError;
use pathfinder_common::types::{SemanticPath, TrustLevel};
use rmcp::model::{ErrorCode, ErrorData};
use std::fmt::Write;
use std::path::Path;
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn millis_to_u64(millis: u128) -> u64 {
millis as u64
}
pub(crate) fn pathfinder_to_error_data(err: &PathfinderError) -> ErrorData {
let err_resp = err.to_error_response();
let data = match serde_json::to_value(&err_resp) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
error = %e,
error_type = std::any::type_name::<PathfinderError>(),
"pathfinder_to_error_data: serialization failed, error context will be lost"
);
None
}
};
let error_code = match err {
pathfinder_common::error::PathfinderError::FileNotFound { .. }
| pathfinder_common::error::PathfinderError::SymbolNotFound { .. }
| pathfinder_common::error::PathfinderError::AmbiguousSymbol { .. }
| pathfinder_common::error::PathfinderError::InvalidSemanticPath { .. }
| pathfinder_common::error::PathfinderError::UnsupportedLanguage { .. }
| pathfinder_common::error::PathfinderError::TokenBudgetExceeded { .. } => {
ErrorCode::INVALID_PARAMS
}
pathfinder_common::error::PathfinderError::AccessDenied { .. } => ErrorCode(-32001),
pathfinder_common::error::PathfinderError::IoError { .. }
| pathfinder_common::error::PathfinderError::ParseError { .. }
| pathfinder_common::error::PathfinderError::LspError { .. }
| pathfinder_common::error::PathfinderError::LspTimeout { .. }
| pathfinder_common::error::PathfinderError::NoLspAvailable { .. }
| pathfinder_common::error::PathfinderError::PathTraversal { .. } => {
ErrorCode::INTERNAL_ERROR
}
};
let mut detailed_message = format!("{}: {}", err_resp.error, err_resp.message);
if let Some(ref hint) = err_resp.hint {
let _ = write!(detailed_message, " Hint: {hint}");
}
if let Some(matches) = err_resp.details.get("matches") {
if let Some(arr) = matches.as_array() {
if !arr.is_empty() {
let match_list: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(ToOwned::to_owned))
.collect();
if !match_list.is_empty() {
let _ = write!(
detailed_message,
" Matches found: {}",
match_list.join(", ")
);
}
}
}
}
ErrorData::new(error_code, detailed_message, data)
}
pub(crate) fn treesitter_error_to_error_data(e: pathfinder_treesitter::SurgeonError) -> ErrorData {
pathfinder_to_error_data(&e.into())
}
pub(crate) fn io_error_data(msg: impl Into<std::borrow::Cow<'static, str>>) -> ErrorData {
ErrorData::internal_error(msg, None)
}
pub(crate) fn invalid_params_error(msg: impl Into<std::borrow::Cow<'static, str>>) -> ErrorData {
ErrorData::invalid_params(msg, None)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn format_degraded_notice(reason: &pathfinder_common::types::DegradedReason) -> String {
let guidance = reason.guidance();
let mut parts = vec![format!("DEGRADED ({reason})")];
match guidance.trust_level {
TrustLevel::Unreliable => {
parts.push("results are UNRELIABLE, do not trust empty counts".into());
}
TrustLevel::Heuristic => {
parts.push("results are heuristic (grep-based), verify manually".into());
}
TrustLevel::Partial => parts.push("results are PARTIAL, some features unavailable".into()),
TrustLevel::None => parts.push("results are UNAVAILABLE for this language".into()),
}
if let Some(fallback) = &guidance.fallback_tool {
parts.push(format!(
"fallback: use {fallback} for authoritative results"
));
}
if let Some(secs) = guidance.retry_after_seconds {
parts.push(format!("retry after ~{secs}s for LSP-backed results"));
}
parts.join(" — ")
}
pub(crate) fn language_from_path(path: &Path) -> &'static str {
match path.extension().and_then(|e| e.to_str()) {
Some("ts" | "tsx") => "typescript",
Some("js" | "jsx" | "mjs" | "cjs") => "javascript",
Some("rs") => "rust",
Some("go") => "go",
Some("py") => "python",
Some("java") => "java",
Some("json") => "json",
Some("yaml" | "yml") => "yaml",
Some("toml") => "toml",
Some("md" | "mdx") => "markdown",
Some("sh" | "bash") => "shell",
Some("dockerfile") | None
if path.file_name().and_then(|n| n.to_str()) == Some("Dockerfile") =>
{
"dockerfile"
}
_ => "text",
}
}
pub(crate) fn parse_semantic_path(raw: &str) -> Result<SemanticPath, ErrorData> {
SemanticPath::parse(raw).ok_or_else(|| {
pathfinder_to_error_data(&PathfinderError::InvalidSemanticPath {
input: raw.to_owned(),
issue: "Semantic path is malformed or missing '::' separator.".to_owned(),
})
})
}
pub(crate) fn require_symbol_target(
semantic_path: &SemanticPath,
raw_path: &str,
) -> Result<(), ErrorData> {
if semantic_path.is_bare_file() {
return Err(pathfinder_to_error_data(
&PathfinderError::InvalidSemanticPath {
input: raw_path.to_owned(),
issue: "this tool requires a symbol target — use 'file.rs::symbol' format"
.to_owned(),
},
));
}
Ok(())
}
pub(crate) fn serialize_metadata<T: serde::Serialize>(metadata: &T) -> Option<serde_json::Value> {
match serde_json::to_value(metadata) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
error = %e,
type_name = std::any::type_name::<T>(),
"structured metadata serialization failed; agent will receive null"
);
None
}
}
}
#[cfg(test)]
#[path = "helpers_test.rs"]
mod tests;