use crate::skill::inventory::McpInventory;
use crate::skill::manifest::ProcedureStep;
use crate::skill::mcp::{McpRequirement, SkillCapability};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
Literal { tool: String },
Hint { tool: String },
IntentMatch {
tool: String,
capability: SkillCapability,
},
Fallback {
tool: String,
capability: SkillCapability,
},
Unresolved { reason: String },
}
impl Resolution {
pub fn picked_tool(&self) -> Option<&str> {
match self {
Resolution::Literal { tool }
| Resolution::Hint { tool }
| Resolution::IntentMatch { tool, .. }
| Resolution::Fallback { tool, .. } => Some(tool),
Resolution::Unresolved { .. } => None,
}
}
pub fn source_tag(&self) -> &'static str {
match self {
Resolution::Literal { .. } => "literal",
Resolution::Hint { .. } => "hint",
Resolution::IntentMatch { .. } => "intent_match",
Resolution::Fallback { .. } => "fallback",
Resolution::Unresolved { .. } => "unresolved",
}
}
}
pub fn resolve_step(
step: &ProcedureStep,
requirements: &[McpRequirement],
inventory: &McpInventory,
) -> Resolution {
if step.intent.is_none() {
if let Some(t) = step.tool.as_deref() {
return Resolution::Literal {
tool: t.to_string(),
};
}
return Resolution::Unresolved {
reason: "step has neither tool nor intent".into(),
};
}
if let Some(hint) = step.tool_hint.as_deref()
&& let Some(t) = match_in_inventory(hint, inventory)
{
return Resolution::Hint { tool: t };
}
let mut best: Option<(String, SkillCapability)> = None;
for req in requirements {
let Ok(glob) = globset::Glob::new(&req.tool_pattern) else {
continue;
};
let matcher = glob.compile_matcher();
let mut candidates: Vec<&str> = inventory.iter().filter(|t| matcher.is_match(t)).collect();
if candidates.is_empty() {
continue;
}
candidates.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
let pick = candidates[0].to_string();
best = Some((pick, req.capability));
break;
}
if let Some((tool, cap)) = best {
return Resolution::IntentMatch {
tool,
capability: cap,
};
}
for req in requirements {
if req.fallback.is_empty() {
continue;
}
if inventory.contains(&req.fallback) {
return Resolution::Fallback {
tool: req.fallback.clone(),
capability: req.capability,
};
}
}
Resolution::Unresolved {
reason: format!(
"intent '{}' has no matching tool in inventory ({} tools, {} requirements)",
step.intent.as_deref().unwrap_or(""),
inventory.iter().count(),
requirements.len(),
),
}
}
fn match_in_inventory(pattern: &str, inv: &McpInventory) -> Option<String> {
if pattern.contains('*') {
let Ok(g) = globset::Glob::new(pattern) else {
return None;
};
let m = g.compile_matcher();
let mut hits: Vec<&str> = inv.iter().filter(|t| m.is_match(t)).collect();
hits.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
hits.first().map(|s| s.to_string())
} else {
inv.contains(pattern).then(|| pattern.to_string())
}
}