Skip to content

Commit 5fbab7b

Browse files
authored
Merge pull request #1703 from rudi193-cmd/fix/1542-trace-json-evidence
fix(mcp): emit trace evidence columns in header order
2 parents 1778637 + 359f736 commit 5fbab7b

2 files changed

Lines changed: 163 additions & 20 deletions

File tree

src/mcp/mcp.c

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6507,25 +6507,33 @@ static bool bfs_edge_evidence_for_hop(cbm_traverse_result_t *tr, int64_t hop_nod
65076507
}
65086508

65096509
/* TOON table for one trace direction: callees[N]{qn,hop,...} with optional
6510-
* risk / test / args columns. `name` is omitted (it is the qn's last
6511-
* segment); the per-item JSON key envelope was 84% of the legacy payload. */
6510+
* risk / test / evidence / args columns. `name` is omitted (it is the qn's
6511+
* last segment); the per-item JSON key envelope was 84% of the legacy
6512+
* payload. include_evidence must be forwarded here — flat_trace used to
6513+
* drop it (#1542 leftover). */
65126514
static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result_t *tr,
6513-
bool risk_labels, bool include_tests, bool data_flow) {
6515+
bool risk_labels, bool include_tests, bool data_flow,
6516+
bool include_evidence) {
65146517
int visible = 0;
65156518
for (int i = 0; i < tr->visited_count; i++) {
65166519
if (!include_tests && is_test_file(tr->visited[i].node.file_path)) {
65176520
continue;
65186521
}
65196522
visible++;
65206523
}
6521-
const char *cols[5] = {"qn", "hop"};
6524+
/* Max: qn hop risk test strategy confidence args. */
6525+
const char *cols[7] = {"qn", "hop"};
65226526
int ncols = 2;
65236527
if (risk_labels) {
65246528
cols[ncols++] = "risk";
65256529
}
65266530
if (include_tests) {
65276531
cols[ncols++] = "test";
65286532
}
6533+
if (include_evidence) {
6534+
cols[ncols++] = "strategy";
6535+
cols[ncols++] = "confidence";
6536+
}
65296537
if (data_flow) {
65306538
cols[ncols++] = "args";
65316539
}
@@ -6545,6 +6553,23 @@ static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
65456553
if (include_tests) {
65466554
cbm_tree_cell_bool(sb, test, false);
65476555
}
6556+
if (include_evidence) {
6557+
const char *ev_class = NULL;
6558+
double ev_conf = -1.0;
6559+
if (bfs_edge_evidence_for_hop(tr, tr->visited[i].node.id, &ev_class, &ev_conf)) {
6560+
cbm_tree_cell_str(sb, ev_class ? ev_class : "", false);
6561+
if (ev_conf >= 0.0) {
6562+
cbm_tree_cell_real(sb, ev_conf, false);
6563+
} else {
6564+
cbm_tree_cell_str(sb, "-", false);
6565+
}
6566+
} else {
6567+
/* Root hop / non-CALLS: keep column count fixed, same "-"
6568+
* placeholders as bfs_to_tree_table. */
6569+
cbm_tree_cell_str(sb, "-", false);
6570+
cbm_tree_cell_str(sb, "-", false);
6571+
}
6572+
}
65486573
if (data_flow) {
65496574
size_t alen = 0;
65506575
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
@@ -6890,20 +6915,9 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
68906915
if (risk_labels) {
68916916
yyjson_mut_arr_add_str(doc, row, cbm_risk_label(cbm_hop_to_risk(tr->visited[i].hop)));
68926917
}
6893-
if (data_flow) {
6894-
size_t alen = 0;
6895-
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
6896-
if (ea && alen > 0) {
6897-
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
6898-
if (av) {
6899-
yyjson_mut_arr_add_val(row, av);
6900-
} else {
6901-
yyjson_mut_arr_add_str(doc, row, "");
6902-
}
6903-
} else {
6904-
yyjson_mut_arr_add_str(doc, row, "");
6905-
}
6906-
}
6918+
/* Emit in header order: strategy, confidence, then args. Swapping these
6919+
* two blocks mislabeled every include_evidence+data_flow json row
6920+
* (#1542 leftover). */
69076921
if (include_evidence) {
69086922
const char *ev_class = NULL;
69096923
double ev_conf = -1.0;
@@ -6923,6 +6937,20 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
69236937
yyjson_mut_arr_add_null(doc, row);
69246938
}
69256939
}
6940+
if (data_flow) {
6941+
size_t alen = 0;
6942+
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
6943+
if (ea && alen > 0) {
6944+
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
6945+
if (av) {
6946+
yyjson_mut_arr_add_val(row, av);
6947+
} else {
6948+
yyjson_mut_arr_add_str(doc, row, "");
6949+
}
6950+
} else {
6951+
yyjson_mut_arr_add_str(doc, row, "");
6952+
}
6953+
}
69266954
yyjson_mut_arr_add_val(cur_rows, row);
69276955
}
69286956
yyjson_mut_obj_add_val(doc, leg, "groups", groups);
@@ -7335,15 +7363,17 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
73357363
if (do_outbound) {
73367364
cbm_tree_scalar_int(&sb, "callees_total", out_total);
73377365
if (flat_trace) {
7338-
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow);
7366+
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow,
7367+
include_evidence);
73397368
} else {
73407369
bfs_to_tree_table(&sb, "callees", &view_out, include_tests, include_evidence);
73417370
}
73427371
}
73437372
if (do_inbound) {
73447373
cbm_tree_scalar_int(&sb, "callers_total", in_total);
73457374
if (flat_trace) {
7346-
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow);
7375+
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow,
7376+
include_evidence);
73477377
} else {
73487378
bfs_to_tree_table(&sb, "callers", &view_in, include_tests, include_evidence);
73497379
}

tests/test_mcp.c

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3725,6 +3725,118 @@ TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped) {
37253725
PASS();
37263726
}
37273727

3728+
/* #1542 leftover: header order is strategy,confidence then args, but json
3729+
* used to emit args first; tree flat_trace (risk_labels || data_flow) used
3730+
* to call bfs_to_toon_table without include_evidence. Pin both: every row
3731+
* has len(cols)==len(row), and the strategy cell is the class not the args
3732+
* array. */
3733+
TEST(tool_trace_path_evidence_columns_match_header_issue1542) {
3734+
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
3735+
cbm_store_t *st = cbm_mcp_server_store(srv);
3736+
const char *proj = "ev-order";
3737+
cbm_mcp_server_set_project(srv, proj);
3738+
cbm_store_upsert_project(st, proj, "/tmp/ev-order");
3739+
cbm_node_t caller = {.project = proj,
3740+
.label = "Function",
3741+
.name = "caller",
3742+
.qualified_name = "ev-order.src.caller",
3743+
.file_path = "src/a.c",
3744+
.start_line = 1,
3745+
.end_line = 5};
3746+
cbm_node_t callee = {.project = proj,
3747+
.label = "Function",
3748+
.name = "target",
3749+
.qualified_name = "ev-order.src.target",
3750+
.file_path = "src/a.c",
3751+
.start_line = 10,
3752+
.end_line = 20};
3753+
int64_t id_caller = cbm_store_upsert_node(st, &caller);
3754+
int64_t id_callee = cbm_store_upsert_node(st, &callee);
3755+
ASSERT_GT(id_caller, 0);
3756+
ASSERT_GT(id_callee, 0);
3757+
cbm_edge_t e = {.project = proj,
3758+
.source_id = id_caller,
3759+
.target_id = id_callee,
3760+
.type = "CALLS",
3761+
.properties_json = "{\"callee\":\"target\",\"confidence\":0.95,"
3762+
"\"strategy\":\"lsp_trait_dispatch\",\"candidates\":1,"
3763+
"\"args\":[\"x\"]}"};
3764+
ASSERT_GT(cbm_store_insert_edge(st, &e), 0);
3765+
3766+
/* json × data_flow × include_evidence: cols identity, not just count. */
3767+
char *js = cbm_mcp_server_handle(
3768+
srv, "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"tools/call\","
3769+
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3770+
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3771+
"\"mode\":\"data_flow\",\"format\":\"json\"}}}");
3772+
ASSERT_NOT_NULL(js);
3773+
char *js_txt = extract_text_content(js);
3774+
ASSERT_NOT_NULL(js_txt);
3775+
yyjson_doc *doc = yyjson_read(js_txt, strlen(js_txt), 0);
3776+
ASSERT_NOT_NULL(doc);
3777+
yyjson_val *callees = yyjson_obj_get(yyjson_doc_get_root(doc), "callees");
3778+
ASSERT_NOT_NULL(callees);
3779+
yyjson_val *cols = yyjson_obj_get(callees, "cols");
3780+
ASSERT_NOT_NULL(cols);
3781+
static const char *want[] = {"name", "hop", "strategy", "confidence", "args"};
3782+
ASSERT_EQ((int)yyjson_arr_size(cols), 5);
3783+
for (int i = 0; i < 5; i++) {
3784+
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(cols, i)), want[i]);
3785+
}
3786+
yyjson_val *hop1 = NULL;
3787+
yyjson_val *groups = yyjson_obj_get(callees, "groups");
3788+
ASSERT_NOT_NULL(groups);
3789+
size_t ng = yyjson_arr_size(groups);
3790+
for (size_t g = 0; g < ng; g++) {
3791+
yyjson_val *rows = yyjson_obj_get(yyjson_arr_get(groups, g), "rows");
3792+
if (!rows) {
3793+
continue;
3794+
}
3795+
size_t nr = yyjson_arr_size(rows);
3796+
for (size_t r = 0; r < nr; r++) {
3797+
yyjson_val *row = yyjson_arr_get(rows, r);
3798+
yyjson_val *hop = row ? yyjson_arr_get(row, 1) : NULL;
3799+
if (hop && yyjson_get_int(hop) >= 1) {
3800+
hop1 = row;
3801+
break;
3802+
}
3803+
}
3804+
if (hop1) {
3805+
break;
3806+
}
3807+
}
3808+
ASSERT_NOT_NULL(hop1);
3809+
ASSERT_EQ((int)yyjson_arr_size(hop1), 5);
3810+
ASSERT_TRUE(yyjson_is_str(yyjson_arr_get(hop1, 2)));
3811+
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(hop1, 2)), "lsp");
3812+
ASSERT_TRUE(yyjson_is_num(yyjson_arr_get(hop1, 3)));
3813+
ASSERT_TRUE(yyjson_is_arr(yyjson_arr_get(hop1, 4)));
3814+
yyjson_doc_free(doc);
3815+
free(js_txt);
3816+
free(js);
3817+
3818+
/* tree × risk_labels × include_evidence used to drop evidence entirely
3819+
* because flat_trace routed through bfs_to_toon_table without the flag. */
3820+
char *tree = cbm_mcp_server_handle(
3821+
srv, "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\","
3822+
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3823+
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3824+
"\"risk_labels\":true}}}");
3825+
ASSERT_NOT_NULL(tree);
3826+
char *tree_txt = extract_text_content(tree);
3827+
ASSERT_NOT_NULL(tree_txt);
3828+
ASSERT_NOT_NULL(strstr(tree_txt, "strategy"));
3829+
ASSERT_NOT_NULL(strstr(tree_txt, "confidence"));
3830+
ASSERT_NOT_NULL(strstr(tree_txt, "lsp"));
3831+
ASSERT_NOT_NULL(strstr(tree_txt, "0.95"));
3832+
ASSERT_NULL(strstr(tree_txt, "lsp_trait_dispatch"));
3833+
free(tree_txt);
3834+
free(tree);
3835+
3836+
cbm_mcp_server_free(srv);
3837+
PASS();
3838+
}
3839+
37283840
/* Reproduce-first (#887): the client-supplied `depth` on trace_call_path must be
37293841
* clamped to the MCP ceiling (cbm_mcp_max_depth(), default 15). On origin/main
37303842
* an MCP_MAX_DEPTH=15 constant was defined but never applied — `depth` flowed
@@ -13757,6 +13869,7 @@ SUITE(mcp) {
1375713869
RUN_TEST(tool_trace_call_path_prefers_definition);
1375813870
RUN_TEST(trace_evidence_strategy_class_vocabulary_is_closed);
1375913871
RUN_TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped);
13872+
RUN_TEST(tool_trace_path_evidence_columns_match_header_issue1542);
1376013873
RUN_TEST(tool_trace_call_path_depth_clamped);
1376113874
RUN_TEST(tool_trace_call_path_distinct_defs_not_over_unioned);
1376213875
RUN_TEST(tool_trace_call_path_dts_stub_unions_with_impl);

0 commit comments

Comments
 (0)