From e71e323bb8d5b998cd9a717bf06bacbf18050603 Mon Sep 17 00:00:00 2001 From: "sds.rs" Date: Fri, 27 Mar 2026 22:33:39 +0800 Subject: [PATCH] fix(map): exclude test callers from hot_functions and improve UX hot_functions in project_map was counting test callers, inflating counts (e.g., extract_relations showed 35 callers but had 0 production callers). Fix the SQL to split prod/test counts and display both ("47 callers + 52 test"). Also adds two UX improvements: - search: detect code syntax queries (parens, ->, ::) and suggest ast-search - similar: provide actionable guidance when embeddings unavailable Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.rs | 57 +++++++++++++++++++++++++++++++---------- src/mcp/server/tools.rs | 31 +++++++++++++++------- src/storage/queries.rs | 27 ++++++++++++++----- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 6b5f21d..4e1de45 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -544,6 +544,18 @@ pub fn cmd_search(project_root: &Path, args: &[String]) -> Result<()> { let fts_result = queries::fts5_search(conn, query, fetch_limit)?; if fts_result.nodes.is_empty() { eprintln!("[code-graph] No results for: {}", query); + // Hint: if query looks like code syntax, suggest ast-search + if query.contains('(') || query.contains(')') || query.contains("->") || query.contains("::") || query.contains('<') { + // Replace non-word chars with spaces, collapse multiple spaces, extract clean keywords + let clean: String = query.chars() + .map(|c| if c.is_alphanumeric() || c == '_' { c } else { ' ' }) + .collect(); + let keywords: Vec<&str> = clean.split_whitespace().collect(); + if !keywords.is_empty() { + eprintln!(" Tip: For structural queries, try: code-graph-mcp ast-search --type fn --returns \"{}\"", + keywords.join(" ")); + } + } return Ok(()); } @@ -1056,12 +1068,18 @@ pub fn cmd_map(project_root: &Path, args: &[String]) -> Result<()> { "handler": e.handler, "file": e.file, })).collect::>(), - "hot_functions": hot_functions.iter().map(|h| serde_json::json!({ - "name": h.name, - "type": h.node_type, - "file": h.file, - "callers": h.caller_count, - })).collect::>(), + "hot_functions": hot_functions.iter().map(|h| { + let mut obj = serde_json::json!({ + "name": h.name, + "type": h.node_type, + "file": h.file, + "callers": h.caller_count, + }); + if h.test_caller_count > 0 { + obj["test_callers"] = serde_json::json!(h.test_caller_count); + } + obj + }).collect::>(), }); writeln!(stdout, "{}", serde_json::to_string(&result)?)?; return Ok(()); @@ -1114,11 +1132,19 @@ pub fn cmd_map(project_root: &Path, args: &[String]) -> Result<()> { writeln!(stdout, "Hot Functions:")?; let max_hot = if compact { 5 } else { hot_functions.len() }; for h in hot_functions.iter().take(max_hot) { - writeln!( - stdout, - " {} ({}) — {} callers ({})", - h.name, h.node_type, h.caller_count, h.file - )?; + if h.test_caller_count > 0 { + writeln!( + stdout, + " {} ({}) — {} callers + {} test ({})", + h.name, h.node_type, h.caller_count, h.test_caller_count, h.file + )?; + } else { + writeln!( + stdout, + " {} ({}) — {} callers ({})", + h.name, h.node_type, h.caller_count, h.file + )?; + } } } @@ -1632,7 +1658,9 @@ pub fn cmd_similar(project_root: &Path, args: &[String]) -> Result<()> { let conn = db.conn(); if !db.vec_enabled() { - eprintln!("[code-graph] Embedding not available. Build with --features embed-model."); + eprintln!("[code-graph] Vector search not available (sqlite-vec extension not loaded)."); + eprintln!(" To enable: build with `cargo build --release --features embed-model`."); + eprintln!(" Alternative: use `code-graph-mcp search ` for text-based similarity."); return Ok(()); } @@ -1658,7 +1686,10 @@ pub fn cmd_similar(project_root: &Path, args: &[String]) -> Result<()> { // Check embedding exists let (embedded_count, total_nodes) = queries::count_nodes_with_vectors(conn)?; if embedded_count == 0 { - eprintln!("[code-graph] No embeddings found ({}/{} nodes embedded). Run MCP server with embed-model feature.", embedded_count, total_nodes); + eprintln!("[code-graph] No embeddings found ({}/{} nodes embedded).", embedded_count, total_nodes); + eprintln!(" To enable: build with `cargo build --release --features embed-model`,"); + eprintln!(" then restart the MCP server to generate embeddings."); + eprintln!(" Alternative: use `code-graph-mcp search ` for text-based similarity."); std::process::exit(1); } diff --git a/src/mcp/server/tools.rs b/src/mcp/server/tools.rs index 2a9f164..4305c65 100644 --- a/src/mcp/server/tools.rs +++ b/src/mcp/server/tools.rs @@ -297,8 +297,11 @@ impl McpServer { } // end estimated_tokens check if results.is_empty() { + let has_code_syntax = query.contains('(') || query.contains(')') || query.contains("->") || query.contains("::") || query.contains('<'); let has_non_ascii = !query.is_ascii(); - let hint = if has_non_ascii { + let hint = if has_code_syntax { + "Query looks like code syntax. For structural queries, use ast_search with type/returns/params filters instead of text search." + } else if has_non_ascii { "Try using English keywords — the search index is English-optimized. Also try broader terms or check spelling." } else { "Try broader terms, check spelling, or use different keywords. The index may need rebuilding if the codebase changed significantly." @@ -1537,12 +1540,16 @@ impl McpServer { }).collect(); let hot_json: Vec = hot_functions.iter().map(|h| { - json!({ + let mut obj = json!({ "name": h.name, "type": h.node_type, "file": h.file, "caller_count": h.caller_count, - }) + }); + if h.test_caller_count > 0 { + obj["test_caller_count"] = json!(h.test_caller_count); + } + obj }).collect(); let r = json!({ @@ -1585,13 +1592,19 @@ impl McpServer { })).collect()) .unwrap_or_default(); - // Trim hot_functions: top 10, name+file only + // Trim hot_functions: top 10, name+file+counts only let compact_hot: Vec = result["hot_functions"].as_array() - .map(|arr| arr.iter().take(10).map(|h| json!({ - "name": h["name"], - "file": h["file"], - "caller_count": h["caller_count"], - })).collect()) + .map(|arr| arr.iter().take(10).map(|h| { + let mut obj = json!({ + "name": h["name"], + "file": h["file"], + "caller_count": h["caller_count"], + }); + if h.get("test_caller_count").and_then(|v| v.as_i64()).unwrap_or(0) > 0 { + obj["test_caller_count"] = h["test_caller_count"].clone(); + } + obj + }).collect()) .unwrap_or_default(); // Trim entry_points: file+handler only diff --git a/src/storage/queries.rs b/src/storage/queries.rs index e8b3fd9..e73296d 100644 --- a/src/storage/queries.rs +++ b/src/storage/queries.rs @@ -1339,6 +1339,7 @@ pub struct HotFunction { pub node_type: String, pub file: String, pub caller_count: usize, + pub test_caller_count: usize, } /// Get the directory part of a file path (everything before the last '/'). @@ -1533,28 +1534,42 @@ pub fn get_project_map(conn: &Connection) -> Result<(Vec, Vec 0 \ + ORDER BY prod_cnt DESC \ LIMIT 15"; let mut stmt = conn.prepare(sql)?; let rows = stmt.query_map([REL_CALLS], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, i64>(3)? as usize)) + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, + row.get::<_, i64>(3)? as usize, row.get::<_, i64>(4)? as usize)) })?; for row in rows { - let (name, node_type, file, count) = row?; - hot_functions.push(HotFunction { name, node_type, file, caller_count: count }); + let (name, node_type, file, count, test_count) = row?; + hot_functions.push(HotFunction { name, node_type, file, caller_count: count, test_caller_count: test_count }); } }