Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 44 additions & 13 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}

Expand Down Expand Up @@ -1056,12 +1068,18 @@ pub fn cmd_map(project_root: &Path, args: &[String]) -> Result<()> {
"handler": e.handler,
"file": e.file,
})).collect::<Vec<_>>(),
"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::<Vec<_>>(),
"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::<Vec<_>>(),
});
writeln!(stdout, "{}", serde_json::to_string(&result)?)?;
return Ok(());
Expand Down Expand Up @@ -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
)?;
}
}
}

Expand Down Expand Up @@ -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 <query>` for text-based similarity.");
return Ok(());
}

Expand All @@ -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 <query>` for text-based similarity.");
std::process::exit(1);
}

Expand Down
31 changes: 22 additions & 9 deletions src/mcp/server/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -1537,12 +1540,16 @@ impl McpServer {
}).collect();

let hot_json: Vec<serde_json::Value> = 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!({
Expand Down Expand Up @@ -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<serde_json::Value> = 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
Expand Down
27 changes: 21 additions & 6 deletions src/storage/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 '/').
Expand Down Expand Up @@ -1533,28 +1534,42 @@ pub fn get_project_map(conn: &Connection) -> Result<(Vec<ModuleStats>, Vec<Modul
}
}

// 5. Hot functions (C1: filter test code, C3: use REL_CALLS constant)
// 5. Hot functions (C1: filter test code, split prod/test caller counts, C3: use REL_CALLS constant)
let mut hot_functions = Vec::new();
{
let sql = "SELECT n.name, n.type, f.path, COUNT(e.id) as cnt \
let sql = "SELECT n.name, n.type, f.path, \
COUNT(CASE WHEN src.is_test = 0 \
AND src.name NOT LIKE 'test\\_%' ESCAPE '\\' \
AND sf.path NOT LIKE 'tests/%' \
AND sf.path NOT LIKE '%_test.%' \
THEN e.id END) as prod_cnt, \
COUNT(CASE WHEN src.is_test = 1 \
OR src.name LIKE 'test\\_%' ESCAPE '\\' \
OR sf.path LIKE 'tests/%' \
OR sf.path LIKE '%_test.%' \
THEN e.id END) as test_cnt \
FROM nodes n \
JOIN files f ON f.id = n.file_id \
JOIN edges e ON e.target_id = n.id \
JOIN nodes src ON src.id = e.source_id \
JOIN files sf ON sf.id = src.file_id \
WHERE e.relation = ?1 AND n.type != 'module' AND n.name != '<module>' \
AND n.is_test = 0 \
AND n.name NOT LIKE 'test\\_%' ESCAPE '\\' \
AND f.path NOT LIKE 'tests/%' \
AND f.path NOT LIKE '%_test.%' \
GROUP BY n.name, n.type, f.path \
ORDER BY cnt DESC \
HAVING prod_cnt > 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 });
}
}

Expand Down
Loading