@@ -2288,6 +2288,66 @@ static yyjson_mut_val *bfs_to_json_array(yyjson_mut_doc *doc, cbm_traverse_resul
22882288 return arr ;
22892289}
22902290
2291+ static char * snippet_suggestions (const char * input , cbm_node_t * nodes , int count );
2292+
2293+ /* Rank a candidate for name resolution. The label tier (callable > class-like >
2294+ * module/file) is the primary key; WITHIN a tier the larger definition by line
2295+ * span wins. In practice the .c-over-.h and C-main-over-shell-main preferences
2296+ * come primarily from span (the real definition has the larger body), since the
2297+ * competing matches usually share a tier — no file extension is hardcoded.
2298+ * Consequence: two same-tier candidates with equal span tie and are reported
2299+ * ambiguous (see pick_resolved_node) rather than guessed. */
2300+ enum {
2301+ RES_RANK_CALLABLE = 2 , /* Function / Method */
2302+ RES_RANK_OTHER = 1 , /* Class / Struct / etc. */
2303+ RES_RANK_MODULE = 0 , /* Module / File */
2304+ RES_LABEL_WEIGHT = 1000000 /* label tier dominates span */
2305+ };
2306+ static long node_resolution_score (const cbm_node_t * n ) {
2307+ long label_rank = RES_RANK_MODULE ;
2308+ if (n -> label ) {
2309+ if (strcmp (n -> label , "Function" ) == 0 || strcmp (n -> label , "Method" ) == 0 ) {
2310+ label_rank = RES_RANK_CALLABLE ;
2311+ } else if (strcmp (n -> label , "Module" ) != 0 && strcmp (n -> label , "File" ) != 0 ) {
2312+ label_rank = RES_RANK_OTHER ;
2313+ }
2314+ }
2315+ long span = (long )n -> end_line - (long )n -> start_line ;
2316+ if (span < 0 ) {
2317+ span = 0 ;
2318+ }
2319+ return label_rank * (long )RES_LABEL_WEIGHT + span ;
2320+ }
2321+
2322+ /* Pick the best-resolving node among name matches. Sets *ambiguous when the top
2323+ * score is shared by more than one candidate (a genuine tie the caller must
2324+ * disambiguate) so resolution never silently traces the wrong same-named node. */
2325+ static int pick_resolved_node (const cbm_node_t * nodes , int count , bool * ambiguous ) {
2326+ * ambiguous = false;
2327+ if (count <= 1 ) {
2328+ return 0 ;
2329+ }
2330+ int best = 0 ;
2331+ long best_score = node_resolution_score (& nodes [0 ]);
2332+ for (int i = 1 ; i < count ; i ++ ) {
2333+ long s = node_resolution_score (& nodes [i ]);
2334+ if (s > best_score ) {
2335+ best_score = s ;
2336+ best = i ;
2337+ }
2338+ }
2339+ int top_count = 0 ;
2340+ for (int i = 0 ; i < count ; i ++ ) {
2341+ if (node_resolution_score (& nodes [i ]) == best_score ) {
2342+ top_count ++ ;
2343+ }
2344+ }
2345+ if (top_count > 1 ) {
2346+ * ambiguous = true;
2347+ }
2348+ return best ;
2349+ }
2350+
22912351static char * handle_trace_call_path (cbm_mcp_server_t * srv , const char * args ) {
22922352 char * func_name = cbm_mcp_get_string_arg (args , "function_name" );
22932353 char * project = cbm_mcp_get_string_arg (args , "project" );
@@ -2372,6 +2432,22 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
23722432 return cbm_mcp_text_result (hint , true);
23732433 }
23742434
2435+ /* Disambiguate same-named matches: prefer the real definition, and report
2436+ * ambiguity (rather than silently tracing nodes[0]) on a genuine tie — e.g.
2437+ * a C main() vs a same-named shell-script main(). */
2438+ bool trace_ambiguous = false;
2439+ int sel = pick_resolved_node (nodes , node_count , & trace_ambiguous );
2440+ if (trace_ambiguous ) {
2441+ char * result = snippet_suggestions (func_name , nodes , node_count );
2442+ free (func_name );
2443+ free (project );
2444+ free (direction );
2445+ free (mode );
2446+ free (param_name );
2447+ cbm_store_free_nodes (nodes , node_count );
2448+ return result ;
2449+ }
2450+
23752451 yyjson_mut_doc * doc = yyjson_mut_doc_new (NULL );
23762452 yyjson_mut_val * root = yyjson_mut_obj (doc );
23772453 yyjson_mut_doc_set_root (doc , root );
@@ -2397,14 +2473,14 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
23972473 cbm_traverse_result_t tr_in = {0 };
23982474
23992475 if (do_outbound ) {
2400- cbm_store_bfs (store , nodes [0 ].id , "outbound" , edge_types , edge_type_count , depth ,
2476+ cbm_store_bfs (store , nodes [sel ].id , "outbound" , edge_types , edge_type_count , depth ,
24012477 MCP_BFS_LIMIT , & tr_out );
24022478 yyjson_mut_obj_add_val (doc , root , "callees" ,
24032479 bfs_to_json_array (doc , & tr_out , risk_labels , include_tests ));
24042480 }
24052481
24062482 if (do_inbound ) {
2407- cbm_store_bfs (store , nodes [0 ].id , "inbound" , edge_types , edge_type_count , depth ,
2483+ cbm_store_bfs (store , nodes [sel ].id , "inbound" , edge_types , edge_type_count , depth ,
24082484 MCP_BFS_LIMIT , & tr_in );
24092485 yyjson_mut_obj_add_val (doc , root , "callers" ,
24102486 bfs_to_json_array (doc , & tr_in , risk_labels , include_tests ));
@@ -3046,6 +3122,21 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
30463122 }
30473123
30483124 if (suffix_count > SKIP_ONE ) {
3125+ /* Prefer the real definition (a .c body over a .h declaration, a Function
3126+ * over a Module) so an unambiguous-by-preference match resolves directly
3127+ * instead of forcing a disambiguation round trip; only a genuine tie still
3128+ * returns suggestions. */
3129+ bool snip_ambiguous = false;
3130+ int ssel = pick_resolved_node (suffix_nodes , suffix_count , & snip_ambiguous );
3131+ if (!snip_ambiguous ) {
3132+ copy_node (& suffix_nodes [ssel ], & node );
3133+ cbm_store_free_nodes (suffix_nodes , suffix_count );
3134+ char * result = build_snippet_response (srv , & node , "suffix" , include_neighbors , NULL , 0 );
3135+ free_node_contents (& node );
3136+ free (qn );
3137+ free (project );
3138+ return result ;
3139+ }
30493140 char * result = snippet_suggestions (qn , suffix_nodes , suffix_count );
30503141 cbm_store_free_nodes (suffix_nodes , suffix_count );
30513142 free (qn );
0 commit comments