Skip to content

Commit 9225087

Browse files
author
Your Name
committed
feat(cross-repo): extend project='*' to get_impact and trace_call_path + channel dedup
Three fixes: 1. Cross-repo get_impact (project='*'): Searches _cross_repo.db for the target symbol across all repos. For each repo containing it, opens the per-project DB, resolves Class→Method, runs BFS, and collects depth-grouped impact results. Returns per-repo risk assessment with combined risk level. 2. Cross-repo trace_call_path (project='*'): Same pattern — finds the function in all repos, traces both inbound (callers) and outbound (callees) at depth 2 per repo. Uses cbm_cross_repo_trace_in_project() for BFS with Class→Method and file-level resolution. 3. Channel dedup safety net: After channel detection Pass 2 (file-level constants), deletes file-level ghost entries when a named function entry already exists for the same (channel_name, file_path, project, direction). Applied in both per-project detect_channels (store.c) and cross-repo build (cross_repo.c). Currently deletes 0 rows (no actual ghosts in current data), but prevents future duplicates if both extractors detect the same pattern. All three tools now support project='*' for cross-repo queries: - search_graph(project='*') — cross-repo search - get_impact(project='*') — cross-repo blast radius - trace_call_path(project='*') — cross-repo call chain trace
1 parent d1b05aa commit 9225087

File tree

3 files changed

+267
-1
lines changed

3 files changed

+267
-1
lines changed

src/mcp/mcp.c

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,7 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
10021002

10031003
/* Forward declarations — defined in cross-repo handler section */
10041004
static char *handle_cross_repo_search(cbm_mcp_server_t *srv, const char *args);
1005+
static char *handle_cross_repo_trace_call(cbm_mcp_server_t *srv, const char *args, char *func_name);
10051006
static char *derive_short_project(const char *full_project);
10061007
static void add_trace_steps(yyjson_mut_doc *doc, yyjson_mut_val *parent,
10071008
const char *key, cbm_cross_trace_step_t *steps, int count);
@@ -1430,8 +1431,18 @@ static char *handle_get_process_steps(cbm_mcp_server_t *srv, const char *args) {
14301431
return result;
14311432
}
14321433

1434+
/* Forward declaration */
1435+
static char *handle_cross_repo_impact(cbm_mcp_server_t *srv, const char *args);
1436+
14331437
static char *handle_get_impact(cbm_mcp_server_t *srv, const char *args) {
14341438
char *project = cbm_mcp_get_string_arg(args, "project");
1439+
1440+
/* Cross-repo impact: project="*" searches all repos for the target */
1441+
if (project && strcmp(project, "*") == 0) {
1442+
free(project);
1443+
return handle_cross_repo_impact(srv, args);
1444+
}
1445+
14351446
char *target = cbm_mcp_get_string_arg(args, "target");
14361447
char *direction = cbm_mcp_get_string_arg(args, "direction");
14371448
int max_depth = cbm_mcp_get_int_arg(args, "max_depth", 3);
@@ -2066,6 +2077,13 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) {
20662077
static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
20672078
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
20682079
char *project = cbm_mcp_get_string_arg(args, "project");
2080+
2081+
/* Cross-repo trace: project="*" searches all repos for the function */
2082+
if (project && strcmp(project, "*") == 0) {
2083+
free(project);
2084+
return handle_cross_repo_trace_call(srv, args, func_name);
2085+
}
2086+
20692087
cbm_store_t *store = resolve_store(srv, project);
20702088
char *direction = cbm_mcp_get_string_arg(args, "direction");
20712089
int depth = cbm_mcp_get_int_arg(args, "depth", 3);
@@ -3979,7 +3997,232 @@ static char *handle_generate_embeddings(cbm_mcp_server_t *srv, const char *args)
39793997
return result;
39803998
}
39813999

3982-
/* ── build_cross_repo_index ──────────────────────────────────── */
4000+
/* ── Cross-repo get_impact (project="*") ─────────────────────── */
4001+
4002+
static char *handle_cross_repo_impact(cbm_mcp_server_t *srv, const char *args) {
4003+
(void)srv;
4004+
char *target = cbm_mcp_get_string_arg(args, "target");
4005+
char *direction = cbm_mcp_get_string_arg(args, "direction");
4006+
int max_depth = cbm_mcp_get_int_arg(args, "max_depth", 3);
4007+
if (!direction) direction = heap_strdup("upstream");
4008+
if (max_depth <= 0) max_depth = 3;
4009+
4010+
if (!target || !target[0]) {
4011+
free(target); free(direction);
4012+
return cbm_mcp_text_result("{\"error\":\"target is required\"}", true);
4013+
}
4014+
4015+
cbm_cross_repo_t *cr = cbm_cross_repo_open();
4016+
if (!cr) {
4017+
free(target); free(direction);
4018+
return cbm_mcp_text_result(
4019+
"{\"error\":\"Cross-repo index not built. Run build_cross_repo_index first.\"}", true);
4020+
}
4021+
4022+
/* Find all repos containing the target symbol via BM25 search */
4023+
cbm_cross_search_output_t search_out = {0};
4024+
cbm_cross_repo_search(cr, target, NULL, 0, 20, &search_out);
4025+
4026+
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
4027+
yyjson_mut_val *root = yyjson_mut_obj(doc);
4028+
yyjson_mut_doc_set_root(doc, root);
4029+
4030+
yyjson_mut_obj_add_strcpy(doc, root, "target", target);
4031+
yyjson_mut_obj_add_strcpy(doc, root, "direction", direction);
4032+
yyjson_mut_obj_add_bool(doc, root, "cross_repo", true);
4033+
4034+
bool is_upstream = strcmp(direction, "upstream") == 0;
4035+
const char *bfs_dir = is_upstream ? "inbound" : "outbound";
4036+
const char *edge_types[] = {"CALLS"};
4037+
4038+
char *seen_projects[50] = {0};
4039+
int seen_count = 0;
4040+
4041+
yyjson_mut_val *repos_arr = yyjson_mut_arr(doc);
4042+
int total_affected = 0;
4043+
const char *max_risk = "LOW";
4044+
4045+
for (int si = 0; si < search_out.count && seen_count < 50; si++) {
4046+
cbm_cross_search_result_t *sr = &search_out.results[si];
4047+
if (!sr->project || !sr->name) continue;
4048+
if (strcmp(sr->name, target) != 0) continue;
4049+
if (!sr->label) continue;
4050+
if (strcmp(sr->label, "Function") != 0 && strcmp(sr->label, "Method") != 0 &&
4051+
strcmp(sr->label, "Class") != 0) continue;
4052+
4053+
bool seen = false;
4054+
for (int j = 0; j < seen_count; j++) {
4055+
if (seen_projects[j] && strcmp(seen_projects[j], sr->project) == 0) { seen = true; break; }
4056+
}
4057+
if (seen) continue;
4058+
seen_projects[seen_count++] = heap_strdup(sr->project);
4059+
4060+
char db_path_buf[2048];
4061+
project_db_path(sr->project, db_path_buf, sizeof(db_path_buf));
4062+
cbm_store_t *pstore = cbm_store_open_path_query(db_path_buf);
4063+
if (!pstore) continue;
4064+
4065+
cbm_node_t *nodes = NULL; int ncount = 0;
4066+
cbm_store_find_nodes_by_name(pstore, sr->project, target, &nodes, &ncount);
4067+
4068+
if (ncount > 0) {
4069+
int64_t start_id = nodes[0].id;
4070+
if (nodes[0].label && strcmp(nodes[0].label, "Class") == 0) {
4071+
sqlite3 *pdb = cbm_store_get_db(pstore);
4072+
sqlite3_stmt *ms = NULL;
4073+
sqlite3_prepare_v2(pdb,
4074+
"SELECT target_id FROM edges WHERE source_id=?1 AND type='DEFINES_METHOD' LIMIT 1",
4075+
-1, &ms, NULL);
4076+
if (ms) {
4077+
sqlite3_bind_int64(ms, 1, start_id);
4078+
if (sqlite3_step(ms) == SQLITE_ROW) start_id = sqlite3_column_int64(ms, 0);
4079+
sqlite3_finalize(ms);
4080+
}
4081+
}
4082+
4083+
cbm_traverse_result_t tr = {0};
4084+
cbm_store_bfs(pstore, start_id, bfs_dir, edge_types, 1, max_depth, 50, &tr);
4085+
4086+
yyjson_mut_val *repo_item = yyjson_mut_obj(doc);
4087+
char *short_proj = derive_short_project(sr->project);
4088+
yyjson_mut_obj_add_strcpy(doc, repo_item, "project", short_proj ? short_proj : "");
4089+
yyjson_mut_obj_add_strcpy(doc, repo_item, "project_id", sr->project);
4090+
free(short_proj);
4091+
yyjson_mut_obj_add_strcpy(doc, repo_item, "file_path", sr->file_path ? sr->file_path : "");
4092+
yyjson_mut_obj_add_int(doc, repo_item, "total_affected", tr.visited_count);
4093+
4094+
int d1 = 0;
4095+
yyjson_mut_val *d1_arr = yyjson_mut_arr(doc);
4096+
for (int vi = 0; vi < tr.visited_count; vi++) {
4097+
if (tr.visited[vi].hop == 1) {
4098+
d1++;
4099+
yyjson_mut_val *h = yyjson_mut_obj(doc);
4100+
yyjson_mut_obj_add_strcpy(doc, h, "name",
4101+
tr.visited[vi].node.name ? tr.visited[vi].node.name : "");
4102+
yyjson_mut_obj_add_strcpy(doc, h, "label",
4103+
tr.visited[vi].node.label ? tr.visited[vi].node.label : "");
4104+
yyjson_mut_obj_add_strcpy(doc, h, "file_path",
4105+
tr.visited[vi].node.file_path ? tr.visited[vi].node.file_path : "");
4106+
yyjson_mut_arr_add_val(d1_arr, h);
4107+
}
4108+
}
4109+
yyjson_mut_obj_add_val(doc, repo_item, "d1_will_break", d1_arr);
4110+
4111+
const char *risk = d1 >= 20 ? "CRITICAL" : d1 >= 10 ? "HIGH" : d1 >= 3 ? "MEDIUM" : "LOW";
4112+
yyjson_mut_obj_add_str(doc, repo_item, "risk", risk);
4113+
if (strcmp(risk, "CRITICAL") == 0 ||
4114+
(strcmp(risk, "HIGH") == 0 && strcmp(max_risk, "CRITICAL") != 0) ||
4115+
(strcmp(risk, "MEDIUM") == 0 && strcmp(max_risk, "LOW") == 0))
4116+
max_risk = risk;
4117+
total_affected += tr.visited_count;
4118+
4119+
yyjson_mut_arr_add_val(repos_arr, repo_item);
4120+
cbm_store_traverse_free(&tr);
4121+
}
4122+
cbm_store_free_nodes(nodes, ncount);
4123+
cbm_store_close(pstore);
4124+
}
4125+
4126+
yyjson_mut_obj_add_str(doc, root, "risk", max_risk);
4127+
yyjson_mut_obj_add_int(doc, root, "total_affected", total_affected);
4128+
yyjson_mut_obj_add_int(doc, root, "repos_with_target", seen_count);
4129+
yyjson_mut_obj_add_val(doc, root, "repos", repos_arr);
4130+
4131+
char *json = yy_doc_to_str(doc);
4132+
yyjson_mut_doc_free(doc);
4133+
cbm_cross_search_free(&search_out);
4134+
cbm_cross_repo_close(cr);
4135+
for (int i = 0; i < seen_count; i++) free(seen_projects[i]);
4136+
free(target); free(direction);
4137+
char *result = cbm_mcp_text_result(json, false);
4138+
free(json);
4139+
return result;
4140+
}
4141+
4142+
/* ── Cross-repo trace_call_path (project="*") ────────────────── */
4143+
4144+
static char *handle_cross_repo_trace_call(cbm_mcp_server_t *srv, const char *args,
4145+
char *func_name) {
4146+
(void)srv;
4147+
if (!func_name || !func_name[0]) {
4148+
free(func_name);
4149+
return cbm_mcp_text_result("{\"error\":\"function_name is required\"}", true);
4150+
}
4151+
4152+
cbm_cross_repo_t *cr = cbm_cross_repo_open();
4153+
if (!cr) {
4154+
free(func_name);
4155+
return cbm_mcp_text_result(
4156+
"{\"error\":\"Cross-repo index not built. Run build_cross_repo_index first.\"}", true);
4157+
}
4158+
4159+
cbm_cross_search_output_t search_out = {0};
4160+
cbm_cross_repo_search(cr, func_name, NULL, 0, 30, &search_out);
4161+
4162+
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
4163+
yyjson_mut_val *root = yyjson_mut_obj(doc);
4164+
yyjson_mut_doc_set_root(doc, root);
4165+
yyjson_mut_obj_add_strcpy(doc, root, "function_name", func_name);
4166+
yyjson_mut_obj_add_bool(doc, root, "cross_repo", true);
4167+
4168+
char *seen_projects[50] = {0};
4169+
int seen_count = 0;
4170+
yyjson_mut_val *repos_arr = yyjson_mut_arr(doc);
4171+
4172+
for (int si = 0; si < search_out.count && seen_count < 50; si++) {
4173+
cbm_cross_search_result_t *sr = &search_out.results[si];
4174+
if (!sr->project || !sr->name) continue;
4175+
if (strcmp(sr->name, func_name) != 0) continue;
4176+
if (!sr->label) continue;
4177+
if (strcmp(sr->label, "Function") != 0 && strcmp(sr->label, "Method") != 0 &&
4178+
strcmp(sr->label, "Class") != 0) continue;
4179+
4180+
bool seen = false;
4181+
for (int j = 0; j < seen_count; j++) {
4182+
if (seen_projects[j] && strcmp(seen_projects[j], sr->project) == 0) { seen = true; break; }
4183+
}
4184+
if (seen) continue;
4185+
seen_projects[seen_count++] = heap_strdup(sr->project);
4186+
4187+
char db_path_buf[2048];
4188+
project_db_path(sr->project, db_path_buf, sizeof(db_path_buf));
4189+
4190+
yyjson_mut_val *repo_item = yyjson_mut_obj(doc);
4191+
char *short_proj = derive_short_project(sr->project);
4192+
yyjson_mut_obj_add_strcpy(doc, repo_item, "project", short_proj ? short_proj : "");
4193+
yyjson_mut_obj_add_strcpy(doc, repo_item, "project_id", sr->project);
4194+
free(short_proj);
4195+
yyjson_mut_obj_add_strcpy(doc, repo_item, "file_path", sr->file_path ? sr->file_path : "");
4196+
yyjson_mut_obj_add_strcpy(doc, repo_item, "label", sr->label);
4197+
4198+
cbm_cross_trace_step_t *in_steps = NULL; int in_count = 0;
4199+
cbm_cross_repo_trace_in_project(db_path_buf, func_name, sr->file_path,
4200+
NULL, "inbound", 2, &in_steps, &in_count);
4201+
if (in_count > 0) add_trace_steps(doc, repo_item, "incoming_calls", in_steps, in_count);
4202+
cbm_cross_trace_free(in_steps, in_count);
4203+
4204+
cbm_cross_trace_step_t *out_steps = NULL; int out_count = 0;
4205+
cbm_cross_repo_trace_in_project(db_path_buf, func_name, sr->file_path,
4206+
NULL, "outbound", 2, &out_steps, &out_count);
4207+
if (out_count > 0) add_trace_steps(doc, repo_item, "outgoing_calls", out_steps, out_count);
4208+
cbm_cross_trace_free(out_steps, out_count);
4209+
4210+
yyjson_mut_arr_add_val(repos_arr, repo_item);
4211+
}
4212+
4213+
yyjson_mut_obj_add_int(doc, root, "repos_with_function", seen_count);
4214+
yyjson_mut_obj_add_val(doc, root, "repos", repos_arr);
4215+
4216+
char *json = yy_doc_to_str(doc);
4217+
yyjson_mut_doc_free(doc);
4218+
cbm_cross_search_free(&search_out);
4219+
cbm_cross_repo_close(cr);
4220+
for (int i = 0; i < seen_count; i++) free(seen_projects[i]);
4221+
free(func_name);
4222+
char *result = cbm_mcp_text_result(json, false);
4223+
free(json);
4224+
return result;
4225+
}
39834226

39844227
/* ── Cross-repo search (search_graph with project="*") ───────── */
39854228

src/store/cross_repo.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,16 @@ cbm_cross_repo_stats_t cbm_cross_repo_build(void) {
364364
if (ins_node) sqlite3_finalize(ins_node);
365365
if (ins_emb) sqlite3_finalize(ins_emb);
366366

367+
/* Suppress file-level ghost channel entries when named entries exist */
368+
sqlite3_exec(db,
369+
"DELETE FROM cross_channels WHERE function_name = '(file-level)' "
370+
"AND EXISTS (SELECT 1 FROM cross_channels c2 "
371+
"WHERE c2.channel_name = cross_channels.channel_name "
372+
"AND c2.file_path = cross_channels.file_path "
373+
"AND c2.project = cross_channels.project "
374+
"AND c2.direction = cross_channels.direction "
375+
"AND c2.function_name != '(file-level)')", NULL, NULL, NULL);
376+
367377
/* Build FTS5 index with camelCase splitting */
368378
sqlite3_exec(db, "DELETE FROM cross_nodes_fts", NULL, NULL, NULL);
369379
sqlite3_exec(db,

src/store/store.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5400,6 +5400,19 @@ int cbm_store_detect_channels(cbm_store_t *s, const char *project, const char *r
54005400
}
54015401

54025402
if (ins) sqlite3_finalize(ins);
5403+
5404+
/* Suppress file-level ghost entries when a named function entry exists
5405+
* for the same channel+file+direction. This prevents duplicate channel
5406+
* entries when both per-node (Pass 1) and file-level (Pass 2) extractors
5407+
* detect the same channel pattern in the same file. */
5408+
exec_sql(s, "DELETE FROM channels WHERE function_name = '(file-level)' "
5409+
"AND EXISTS (SELECT 1 FROM channels c2 "
5410+
"WHERE c2.channel_name = channels.channel_name "
5411+
"AND c2.file_path = channels.file_path "
5412+
"AND c2.project = channels.project "
5413+
"AND c2.direction = channels.direction "
5414+
"AND c2.function_name != '(file-level)')");
5415+
54035416
return total;
54045417
}
54055418

0 commit comments

Comments
 (0)