Skip to content

Commit d1b05aa

Browse files
author
Your Name
committed
feat(cross-repo): search, flow tracing with call chains, and impact analysis
Adds three major cross-repo capabilities building on the _cross_repo.db infrastructure from the previous commit: 1. Cross-repo search (search_graph with project='*'): Dispatches to cbm_cross_repo_search() which runs BM25+vector+RRF against the unified cross-repo index (134K nodes, 134K embeddings). Returns results with both short project name and full project_id for follow-up per-repo queries. 2. Enhanced trace_cross_repo with call chains: When a channel filter is provided, traces upstream callers of the emitter and downstream callees of the listener, depth 2 per hop. New cbm_cross_repo_trace_in_project() helper opens project DBs ad-hoc, resolves Class→Method and (file-level) listeners, runs cbm_store_bfs, returns structured step arrays. Without channel filter: returns channel matches only (unchanged). 3. Cross-repo impact analysis (get_impact with cross_repo=true): After per-repo BFS, checks if any d=1 impacted symbols emit channels in cross_channels. For each affected channel, opens consumer project DB, traces downstream from the listener function, returns cross_repo_impacts array with consumer repo, listener function, downstream affected count, and trace steps. New functions in cross_repo.c: - cbm_cross_repo_trace_in_project: open DB, resolve function, BFS, return steps. Handles Class→Method resolution and file-level listener fallback via channels table node_id lookup. - cbm_cross_trace_free: cleanup for trace step arrays. Changes in mcp.c: - handle_search_graph: early-exit to handle_cross_repo_search when project='*' - handle_cross_repo_search: new handler with hybrid search + JSON output - handle_trace_cross_repo: enhanced with call chain tracing per match - handle_get_impact: cross_repo=true triggers channel + consumer trace No new dependencies. Uses existing cbm_store_bfs and cbm_store_open_path_query.
1 parent 817182a commit d1b05aa

File tree

3 files changed

+401
-2
lines changed

3 files changed

+401
-2
lines changed

src/mcp/mcp.c

Lines changed: 236 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10031009
static 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+
38934073
static 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+
39214117
static 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

Comments
 (0)