@@ -1000,8 +1000,21 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
10001000 return result ;
10011001}
10021002
1003+ /* Forward declarations — defined in cross-repo handler section */
1004+ static char * handle_cross_repo_search (cbm_mcp_server_t * srv , const char * args );
1005+ static char * derive_short_project (const char * full_project );
1006+ static void add_trace_steps (yyjson_mut_doc * doc , yyjson_mut_val * parent ,
1007+ const char * key , cbm_cross_trace_step_t * steps , int count );
1008+
10031009static char * handle_search_graph (cbm_mcp_server_t * srv , const char * args ) {
10041010 char * project = cbm_mcp_get_string_arg (args , "project" );
1011+
1012+ /* Cross-repo search: project="*" dispatches to unified _cross_repo.db */
1013+ if (project && strcmp (project , "*" ) == 0 ) {
1014+ free (project );
1015+ return handle_cross_repo_search (srv , args );
1016+ }
1017+
10051018 cbm_store_t * store = resolve_store (srv , project );
10061019 REQUIRE_STORE (store , project );
10071020
@@ -1422,6 +1435,7 @@ static char *handle_get_impact(cbm_mcp_server_t *srv, const char *args) {
14221435 char * target = cbm_mcp_get_string_arg (args , "target" );
14231436 char * direction = cbm_mcp_get_string_arg (args , "direction" );
14241437 int max_depth = cbm_mcp_get_int_arg (args , "max_depth" , 3 );
1438+ bool cross_repo = cbm_mcp_get_bool_arg (args , "cross_repo" );
14251439 cbm_store_t * store = resolve_store (srv , project );
14261440 REQUIRE_STORE (store , project );
14271441
@@ -1661,6 +1675,83 @@ static char *handle_get_impact(cbm_mcp_server_t *srv, const char *args) {
16611675 cbm_store_free_processes (procs , pcount );
16621676 }
16631677
1678+ /* ── Cross-repo impact: check if d=1 nodes emit channels to other repos ── */
1679+ if (cross_repo && tr .visited_count > 0 ) {
1680+ cbm_cross_repo_t * cr = cbm_cross_repo_open ();
1681+ if (cr ) {
1682+ yyjson_mut_val * xr_arr = yyjson_mut_arr (doc );
1683+ int xr_count = 0 ;
1684+
1685+ /* For each d=1 visited node, check cross_channels for emitters */
1686+ for (int vi = 0 ; vi < tr .visited_count && xr_count < 10 ; vi ++ ) {
1687+ if (tr .visited [vi ].hop != 1 ) continue ; /* only d=1 */
1688+ const char * vname = tr .visited [vi ].node .name ;
1689+ if (!vname ) continue ;
1690+
1691+ /* Query cross_channels for this function as an emitter */
1692+ cbm_cross_channel_match_t * xmatches = NULL ;
1693+ int xmatch_count = 0 ;
1694+ /* Use the function name to filter — imprecise but functional */
1695+ cbm_cross_repo_match_channels (cr , NULL , & xmatches , & xmatch_count );
1696+
1697+ for (int xi = 0 ; xi < xmatch_count && xr_count < 10 ; xi ++ ) {
1698+ /* Match: emitter function name matches d=1 visited node */
1699+ if (!xmatches [xi ].emit_function ) continue ;
1700+ if (strcmp (xmatches [xi ].emit_function , vname ) != 0 ) continue ;
1701+ /* And the emitter project matches our project */
1702+ if (!xmatches [xi ].emit_project || !project ) continue ;
1703+ if (strcmp (xmatches [xi ].emit_project , project ) != 0 ) continue ;
1704+
1705+ /* Found a cross-repo channel triggered by this d=1 node */
1706+ yyjson_mut_val * xr_item = yyjson_mut_obj (doc );
1707+ yyjson_mut_obj_add_strcpy (doc , xr_item , "channel" ,
1708+ xmatches [xi ].channel_name ? xmatches [xi ].channel_name : "" );
1709+ yyjson_mut_obj_add_strcpy (doc , xr_item , "transport" ,
1710+ xmatches [xi ].transport ? xmatches [xi ].transport : "" );
1711+ yyjson_mut_obj_add_strcpy (doc , xr_item , "triggered_by" , vname );
1712+ yyjson_mut_obj_add_int (doc , xr_item , "triggered_by_depth" , 1 );
1713+
1714+ char * lp_short = derive_short_project (
1715+ xmatches [xi ].listen_project ? xmatches [xi ].listen_project : "" );
1716+ yyjson_mut_obj_add_strcpy (doc , xr_item , "consumer_repo" ,
1717+ lp_short ? lp_short : "" );
1718+ yyjson_mut_obj_add_strcpy (doc , xr_item , "consumer_project_id" ,
1719+ xmatches [xi ].listen_project ? xmatches [xi ].listen_project : "" );
1720+ free (lp_short );
1721+
1722+ yyjson_mut_obj_add_strcpy (doc , xr_item , "listener_function" ,
1723+ xmatches [xi ].listen_function ? xmatches [xi ].listen_function : "" );
1724+ yyjson_mut_obj_add_strcpy (doc , xr_item , "listener_file" ,
1725+ xmatches [xi ].listen_file ? xmatches [xi ].listen_file : "" );
1726+
1727+ /* Trace downstream in consumer repo */
1728+ char db_path_buf [2048 ];
1729+ project_db_path (xmatches [xi ].listen_project , db_path_buf , sizeof (db_path_buf ));
1730+ cbm_cross_trace_step_t * ds_steps = NULL ;
1731+ int ds_count = 0 ;
1732+ cbm_cross_repo_trace_in_project (db_path_buf ,
1733+ xmatches [xi ].listen_function , xmatches [xi ].listen_file ,
1734+ xmatches [xi ].channel_name , "outbound" , 2 , & ds_steps , & ds_count );
1735+ yyjson_mut_obj_add_int (doc , xr_item , "downstream_affected" , ds_count );
1736+ if (ds_count > 0 ) {
1737+ add_trace_steps (doc , xr_item , "downstream" , ds_steps , ds_count );
1738+ }
1739+ cbm_cross_trace_free (ds_steps , ds_count );
1740+
1741+ yyjson_mut_arr_add_val (xr_arr , xr_item );
1742+ xr_count ++ ;
1743+ }
1744+ cbm_cross_channel_free (xmatches , xmatch_count );
1745+ }
1746+
1747+ if (xr_count > 0 ) {
1748+ yyjson_mut_obj_add_val (doc , root , "cross_repo_impacts" , xr_arr );
1749+ yyjson_mut_obj_add_int (doc , root , "cross_repo_impact_count" , xr_count );
1750+ }
1751+ cbm_cross_repo_close (cr );
1752+ }
1753+ }
1754+
16641755 char * json = yy_doc_to_str (doc );
16651756 yyjson_mut_doc_free (doc );
16661757 cbm_store_traverse_free (& tr );
@@ -3890,6 +3981,95 @@ static char *handle_generate_embeddings(cbm_mcp_server_t *srv, const char *args)
38903981
38913982/* ── build_cross_repo_index ──────────────────────────────────── */
38923983
3984+ /* ── Cross-repo search (search_graph with project="*") ───────── */
3985+
3986+ static char * derive_short_project (const char * full_project ) {
3987+ /* "mnt-c-Users-Name-Projects-repo-name" → "repo-name" */
3988+ const char * marker = strstr (full_project , "Projects-" );
3989+ if (marker ) return heap_strdup (marker + 9 );
3990+ return heap_strdup (full_project );
3991+ }
3992+
3993+ static char * handle_cross_repo_search (cbm_mcp_server_t * srv , const char * args ) {
3994+ (void )srv ;
3995+
3996+ cbm_cross_repo_t * cr = cbm_cross_repo_open ();
3997+ if (!cr ) {
3998+ return cbm_mcp_text_result (
3999+ "{\"error\":\"Cross-repo index not built. Run build_cross_repo_index first.\"}" , true);
4000+ }
4001+
4002+ char * query = cbm_mcp_get_string_arg (args , "query" );
4003+ int limit = cbm_mcp_get_int_arg (args , "limit" , 30 );
4004+ if (limit <= 0 ) limit = 30 ;
4005+
4006+ if (!query || !query [0 ]) {
4007+ cbm_cross_repo_close (cr );
4008+ free (query );
4009+ return cbm_mcp_text_result (
4010+ "{\"error\":\"query parameter required for cross-repo search\"}" , true);
4011+ }
4012+
4013+ /* Embed query for hybrid search if configured */
4014+ float * query_vec = NULL ;
4015+ int dims = 0 ;
4016+ if (cbm_embedding_is_configured ()) {
4017+ cbm_embedding_config_t cfg = cbm_embedding_get_config ();
4018+ query_vec = cbm_embedding_embed_text (& cfg , query );
4019+ dims = cfg .dims ;
4020+ }
4021+
4022+ cbm_cross_search_output_t out = {0 };
4023+ cbm_cross_repo_search (cr , query , query_vec , dims , limit , & out );
4024+
4025+ yyjson_mut_doc * doc = yyjson_mut_doc_new (NULL );
4026+ yyjson_mut_val * root = yyjson_mut_obj (doc );
4027+ yyjson_mut_doc_set_root (doc , root );
4028+
4029+ yyjson_mut_obj_add_int (doc , root , "total" , out .total );
4030+ yyjson_mut_obj_add_str (doc , root , "search_mode" ,
4031+ out .used_vector ? "hybrid_bm25_vector" : "bm25" );
4032+ yyjson_mut_obj_add_bool (doc , root , "cross_repo" , true);
4033+
4034+ yyjson_mut_val * results = yyjson_mut_arr (doc );
4035+ for (int i = 0 ; i < out .count ; i ++ ) {
4036+ cbm_cross_search_result_t * r = & out .results [i ];
4037+ yyjson_mut_val * item = yyjson_mut_obj (doc );
4038+
4039+ /* Short project name for display */
4040+ char * short_proj = derive_short_project (r -> project ? r -> project : "" );
4041+ yyjson_mut_obj_add_strcpy (doc , item , "project" , short_proj ? short_proj : "" );
4042+ /* Full project ID for follow-up calls */
4043+ yyjson_mut_obj_add_strcpy (doc , item , "project_id" , r -> project ? r -> project : "" );
4044+ free (short_proj );
4045+
4046+ yyjson_mut_obj_add_strcpy (doc , item , "name" , r -> name ? r -> name : "" );
4047+ yyjson_mut_obj_add_strcpy (doc , item , "qualified_name" ,
4048+ r -> qualified_name ? r -> qualified_name : "" );
4049+ yyjson_mut_obj_add_strcpy (doc , item , "label" , r -> label ? r -> label : "" );
4050+ yyjson_mut_obj_add_strcpy (doc , item , "file_path" , r -> file_path ? r -> file_path : "" );
4051+ yyjson_mut_obj_add_real (doc , item , "score" , r -> score );
4052+ if (r -> similarity > 0 )
4053+ yyjson_mut_obj_add_real (doc , item , "similarity" , r -> similarity );
4054+
4055+ yyjson_mut_arr_add_val (results , item );
4056+ }
4057+ yyjson_mut_obj_add_val (doc , root , "results" , results );
4058+
4059+ char * json = yy_doc_to_str (doc );
4060+ yyjson_mut_doc_free (doc );
4061+ cbm_cross_search_free (& out );
4062+ cbm_cross_repo_close (cr );
4063+ free (query );
4064+ free (query_vec );
4065+
4066+ char * result = cbm_mcp_text_result (json , false);
4067+ free (json );
4068+ return result ;
4069+ }
4070+
4071+ /* ── build_cross_repo_index ──────────────────────────────────── */
4072+
38934073static char * handle_build_cross_repo_index (cbm_mcp_server_t * srv , const char * args ) {
38944074 (void )srv ; (void )args ;
38954075
@@ -3918,9 +4098,26 @@ static char *handle_build_cross_repo_index(cbm_mcp_server_t *srv, const char *ar
39184098
39194099/* ── trace_cross_repo ────────────────────────────────────────── */
39204100
4101+ /* Add trace steps as a JSON array to a parent object */
4102+ static void add_trace_steps (yyjson_mut_doc * doc , yyjson_mut_val * parent ,
4103+ const char * key , cbm_cross_trace_step_t * steps , int count ) {
4104+ yyjson_mut_val * arr = yyjson_mut_arr (doc );
4105+ for (int i = 0 ; i < count ; i ++ ) {
4106+ yyjson_mut_val * step = yyjson_mut_obj (doc );
4107+ yyjson_mut_obj_add_strcpy (doc , step , "name" , steps [i ].name ? steps [i ].name : "" );
4108+ yyjson_mut_obj_add_strcpy (doc , step , "label" , steps [i ].label ? steps [i ].label : "" );
4109+ yyjson_mut_obj_add_strcpy (doc , step , "file_path" ,
4110+ steps [i ].file_path ? steps [i ].file_path : "" );
4111+ yyjson_mut_obj_add_int (doc , step , "depth" , steps [i ].depth );
4112+ yyjson_mut_arr_add_val (arr , step );
4113+ }
4114+ yyjson_mut_obj_add_val (doc , parent , key , arr );
4115+ }
4116+
39214117static char * handle_trace_cross_repo (cbm_mcp_server_t * srv , const char * args ) {
39224118 (void )srv ;
39234119 char * channel = cbm_mcp_get_string_arg (args , "channel" );
4120+ bool trace_calls = (channel && channel [0 ]); /* only trace call chains when channel filter given */
39244121
39254122 cbm_cross_repo_t * cr = cbm_cross_repo_open ();
39264123 if (!cr ) {
@@ -3945,6 +4142,7 @@ static char *handle_trace_cross_repo(cbm_mcp_server_t *srv, const char *args) {
39454142 yyjson_mut_obj_add_int (doc , root , "total_repos" , info .total_repos );
39464143 yyjson_mut_obj_add_int (doc , root , "total_cross_repo_channels" , info .cross_repo_channel_count );
39474144 yyjson_mut_obj_add_int (doc , root , "matches" , match_count );
4145+ yyjson_mut_obj_add_bool (doc , root , "call_chains_included" , trace_calls );
39484146 if (info .built_at )
39494147 yyjson_mut_obj_add_strcpy (doc , root , "built_at" , info .built_at );
39504148
@@ -3955,16 +4153,52 @@ static char *handle_trace_cross_repo(cbm_mcp_server_t *srv, const char *args) {
39554153 yyjson_mut_obj_add_strcpy (doc , item , "channel" , m -> channel_name ? m -> channel_name : "" );
39564154 yyjson_mut_obj_add_strcpy (doc , item , "transport" , m -> transport ? m -> transport : "" );
39574155
4156+ /* Emitter side */
39584157 yyjson_mut_val * emit = yyjson_mut_obj (doc );
3959- yyjson_mut_obj_add_strcpy (doc , emit , "project" , m -> emit_project ? m -> emit_project : "" );
4158+ char * ep_short = derive_short_project (m -> emit_project ? m -> emit_project : "" );
4159+ yyjson_mut_obj_add_strcpy (doc , emit , "project" , ep_short ? ep_short : "" );
4160+ free (ep_short );
39604161 yyjson_mut_obj_add_strcpy (doc , emit , "file" , m -> emit_file ? m -> emit_file : "" );
39614162 yyjson_mut_obj_add_strcpy (doc , emit , "function" , m -> emit_function ? m -> emit_function : "" );
4163+
4164+ /* Trace upstream callers of the emitter (what triggers the emit) */
4165+ if (trace_calls && m -> emit_project ) {
4166+ char db_path [2048 ];
4167+ project_db_path (m -> emit_project , db_path , sizeof (db_path ));
4168+ cbm_cross_trace_step_t * steps = NULL ;
4169+ int step_count = 0 ;
4170+ cbm_cross_repo_trace_in_project (db_path , m -> emit_function ,
4171+ m -> emit_file , m -> channel_name ,
4172+ "inbound" , 2 , & steps , & step_count );
4173+ if (step_count > 0 ) {
4174+ add_trace_steps (doc , emit , "upstream" , steps , step_count );
4175+ }
4176+ cbm_cross_trace_free (steps , step_count );
4177+ }
39624178 yyjson_mut_obj_add_val (doc , item , "emitter" , emit );
39634179
4180+ /* Listener side */
39644181 yyjson_mut_val * listen = yyjson_mut_obj (doc );
3965- yyjson_mut_obj_add_strcpy (doc , listen , "project" , m -> listen_project ? m -> listen_project : "" );
4182+ char * lp_short = derive_short_project (m -> listen_project ? m -> listen_project : "" );
4183+ yyjson_mut_obj_add_strcpy (doc , listen , "project" , lp_short ? lp_short : "" );
4184+ free (lp_short );
39664185 yyjson_mut_obj_add_strcpy (doc , listen , "file" , m -> listen_file ? m -> listen_file : "" );
39674186 yyjson_mut_obj_add_strcpy (doc , listen , "function" , m -> listen_function ? m -> listen_function : "" );
4187+
4188+ /* Trace downstream callees of the listener (what the listener triggers) */
4189+ if (trace_calls && m -> listen_project ) {
4190+ char db_path [2048 ];
4191+ project_db_path (m -> listen_project , db_path , sizeof (db_path ));
4192+ cbm_cross_trace_step_t * steps = NULL ;
4193+ int step_count = 0 ;
4194+ cbm_cross_repo_trace_in_project (db_path , m -> listen_function ,
4195+ m -> listen_file , m -> channel_name ,
4196+ "outbound" , 2 , & steps , & step_count );
4197+ if (step_count > 0 ) {
4198+ add_trace_steps (doc , listen , "downstream" , steps , step_count );
4199+ }
4200+ cbm_cross_trace_free (steps , step_count );
4201+ }
39684202 yyjson_mut_obj_add_val (doc , item , "listener" , listen );
39694203
39704204 yyjson_mut_arr_add_val (arr , item );
0 commit comments