include!(concat!(env!("OUT_DIR"), "/tool_registry.rs"));
include!(concat!(env!("OUT_DIR"), "/alias_table.rs"));
include!(concat!(env!("OUT_DIR"), "/trigram_index.rs"));
pub struct DiscoveryService {
trigram_index: TrigramIndex,
}
impl DiscoveryService {
#[must_use]
pub fn new() -> Self {
Self {
trigram_index: TrigramIndex,
}
}
pub fn resolve_tool(&self, query: &str) -> Option<&'static str> {
let normalized = query.to_lowercase();
if normalized.len() < 2 {
return None;
}
if let Some(tool) = TOOL_REGISTRY.get(normalized.as_str()) {
return Some(tool.name);
}
for (tool_name, aliases) in ALIAS_TABLE.iter() {
for alias in aliases {
if normalized.contains(alias) {
return Some(tool_name);
}
}
}
let candidates: Vec<(&'static str, &str)> = TOOL_REGISTRY
.iter()
.map(|(name, meta)| (*name, meta.description))
.collect();
if let Some((best_match, _score)) =
self.trigram_index.find_best_match(&normalized, &candidates)
{
Some(best_match)
} else {
None
}
}
pub fn list_tools(&self) -> Vec<ToolInfo> {
TOOL_REGISTRY
.iter()
.map(|(name, meta)| ToolInfo {
name: (*name).to_string(),
description: meta.description.to_string(),
keywords: meta.keywords.iter().map(|s| (*s).to_string()).collect(),
aliases: ALIAS_TABLE
.get(name)
.map(|aliases| aliases.iter().map(|s| (*s).to_string()).collect())
.unwrap_or_default(),
})
.collect()
}
#[must_use]
pub fn disambiguate<'a>(&self, candidates: Vec<&'a str>, context: Option<&Context>) -> &'a str {
if candidates.is_empty() {
return "";
}
if candidates.len() == 1 {
return candidates[0];
}
if let Some(result) = Self::match_by_extension(&candidates, context) {
return result;
}
Self::pick_by_category_priority(candidates)
}
fn match_by_extension<'a>(
candidates: &[&'a str],
context: Option<&Context>,
) -> Option<&'a str> {
let ext = context.and_then(|ctx| ctx.file_extension.as_deref())?;
match ext {
"rs" if candidates.contains(&"analyze_complexity") => Some("analyze_complexity"),
"ts" | "js" if candidates.contains(&"analyze_dag") => Some("analyze_dag"),
_ => None,
}
}
fn pick_by_category_priority(candidates: Vec<&str>) -> &str {
let mut prioritized: Vec<_> = candidates
.into_iter()
.map(|name| (name, Self::category_priority(name)))
.collect();
prioritized.sort_by_key(|(_, p)| *p);
prioritized[0].0
}
fn category_priority(name: &str) -> u8 {
if name.starts_with("generate") || name.starts_with("scaffold") {
return 0;
}
if name.starts_with("analyze") {
return 1;
}
if name.starts_with("refactor") {
return 2;
}
3
}
}
impl Default for DiscoveryService {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub keywords: Vec<String>,
pub aliases: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Context {
pub file_extension: Option<String>,
pub current_directory: Option<String>,
pub recent_tools: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DiscoveryMetrics {
pub total_queries: u64,
pub exact_matches: u64,
pub alias_matches: u64,
pub fuzzy_matches: u64,
pub no_matches: u64,
pub avg_resolution_time_ns: u64,
}
impl DiscoveryMetrics {
#[must_use]
pub fn success_rate(&self) -> f64 {
if self.total_queries == 0 {
return 0.0;
}
let successful = self.exact_matches + self.alias_matches + self.fuzzy_matches;
successful as f64 / self.total_queries as f64
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exact_match() {
let service = DiscoveryService::new();
assert_eq!(
service.resolve_tool("analyze_complexity"),
Some("analyze_complexity")
);
assert_eq!(service.resolve_tool("quality_gate"), Some("quality_gate"));
}
#[test]
fn test_alias_match() {
let service = DiscoveryService::new();
assert_eq!(
service.resolve_tool("complexity"),
Some("analyze_complexity")
);
assert_eq!(service.resolve_tool("debt"), Some("analyze_satd"));
assert_eq!(service.resolve_tool("technical debt"), Some("analyze_satd"));
}
#[test]
fn test_fuzzy_match() {
let service = DiscoveryService::new();
assert_eq!(
service.resolve_tool("complexity"), Some("analyze_complexity")
);
assert_eq!(service.resolve_tool("refactor"), Some("refactor.start"));
}
#[test]
fn test_no_match() {
let service = DiscoveryService::new();
assert_eq!(service.resolve_tool("xyz123"), None);
}
#[test]
fn test_disambiguation() {
let service = DiscoveryService::new();
let candidates = vec!["analyze_complexity", "generate_context"];
let context = Context {
file_extension: None,
current_directory: None,
recent_tools: vec![],
};
assert_eq!(
service.disambiguate(candidates, Some(&context)),
"generate_context"
);
let candidates = vec!["analyze_dag", "analyze_complexity"];
let context = Context {
file_extension: Some("rs".to_string()),
current_directory: None,
recent_tools: vec![],
};
assert_eq!(
service.disambiguate(candidates, Some(&context)),
"analyze_complexity"
);
}
#[test]
fn test_list_tools() {
let service = DiscoveryService::new();
let tools = service.list_tools();
assert!(!tools.is_empty());
assert!(tools.iter().any(|t| t.name == "analyze_complexity"));
assert!(tools.iter().any(|t| t.name == "quality_gate"));
}
#[test]
fn test_trigram_similarity() {
let index = TrigramIndex;
assert_eq!(index.similarity_score("test", "test"), 1.0);
let score = index.similarity_score("complexity", "complex");
assert!(score > 0.5);
let score = index.similarity_score("abc", "xyz");
assert!(score < 0.1);
}
#[test]
fn test_performance() {
use std::time::Instant;
let service = DiscoveryService::new();
let start = Instant::now();
let _service2 = DiscoveryService::new();
let init_time = start.elapsed();
assert!(
init_time.as_millis() < 10,
"Initialization took {}ms",
init_time.as_millis()
);
let queries = vec![
"analyze_complexity",
"complexity",
"complxity",
"debt",
"quality",
"refactor",
];
let start = Instant::now();
for query in &queries {
let _ = service.resolve_tool(query);
}
let total_time = start.elapsed();
let avg_time = total_time / queries.len() as u32;
assert!(
avg_time.as_millis() < 5,
"Average query time: {}ms",
avg_time.as_millis()
);
}
}
#[cfg(test)]
#[path = "discovery_integration_test.rs"]
mod integration_tests;
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}