Skip to content

Commit 0106ff5

Browse files
fix(mcp): emit trace evidence columns in header order
json + include_evidence + data_flow labeled args as strategy because bfs_to_tree_json declared strategy,confidence before args but emitted args first. Tree flat_trace (risk_labels or data_flow) also dropped include_evidence by calling bfs_to_toon_table without the flag. Fixes DeusData#1542 Signed-off-by: rudi193-cmd <rudi193@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4d7d9f1 commit 0106ff5

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
@@ -5954,25 +5954,33 @@ static bool bfs_edge_evidence_for_hop(cbm_traverse_result_t *tr, int64_t hop_nod
59545954
}
59555955

59565956
/* TOON table for one trace direction: callees[N]{qn,hop,...} with optional
5957-
* risk / test / args columns. `name` is omitted (it is the qn's last
5958-
* segment); the per-item JSON key envelope was 84% of the legacy payload. */
5957+
* risk / test / evidence / args columns. `name` is omitted (it is the qn's
5958+
* last segment); the per-item JSON key envelope was 84% of the legacy
5959+
* payload. include_evidence must be forwarded here — flat_trace used to
5960+
* drop it (#1542 leftover). */
59595961
static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result_t *tr,
5960-
bool risk_labels, bool include_tests, bool data_flow) {
5962+
bool risk_labels, bool include_tests, bool data_flow,
5963+
bool include_evidence) {
59615964
int visible = 0;
59625965
for (int i = 0; i < tr->visited_count; i++) {
59635966
if (!include_tests && is_test_file(tr->visited[i].node.file_path)) {
59645967
continue;
59655968
}
59665969
visible++;
59675970
}
5968-
const char *cols[5] = {"qn", "hop"};
5971+
/* Max: qn hop risk test strategy confidence args. */
5972+
const char *cols[7] = {"qn", "hop"};
59695973
int ncols = 2;
59705974
if (risk_labels) {
59715975
cols[ncols++] = "risk";
59725976
}
59735977
if (include_tests) {
59745978
cols[ncols++] = "test";
59755979
}
5980+
if (include_evidence) {
5981+
cols[ncols++] = "strategy";
5982+
cols[ncols++] = "confidence";
5983+
}
59765984
if (data_flow) {
59775985
cols[ncols++] = "args";
59785986
}
@@ -5992,6 +6000,23 @@ static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
59926000
if (include_tests) {
59936001
cbm_tree_cell_bool(sb, test, false);
59946002
}
6003+
if (include_evidence) {
6004+
const char *ev_class = NULL;
6005+
double ev_conf = -1.0;
6006+
if (bfs_edge_evidence_for_hop(tr, tr->visited[i].node.id, &ev_class, &ev_conf)) {
6007+
cbm_tree_cell_str(sb, ev_class ? ev_class : "", false);
6008+
if (ev_conf >= 0.0) {
6009+
cbm_tree_cell_real(sb, ev_conf, false);
6010+
} else {
6011+
cbm_tree_cell_str(sb, "-", false);
6012+
}
6013+
} else {
6014+
/* Root hop / non-CALLS: keep column count fixed, same "-"
6015+
* placeholders as bfs_to_tree_table. */
6016+
cbm_tree_cell_str(sb, "-", false);
6017+
cbm_tree_cell_str(sb, "-", false);
6018+
}
6019+
}
59956020
if (data_flow) {
59966021
size_t alen = 0;
59976022
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
@@ -6337,20 +6362,9 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
63376362
if (risk_labels) {
63386363
yyjson_mut_arr_add_str(doc, row, cbm_risk_label(cbm_hop_to_risk(tr->visited[i].hop)));
63396364
}
6340-
if (data_flow) {
6341-
size_t alen = 0;
6342-
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
6343-
if (ea && alen > 0) {
6344-
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
6345-
if (av) {
6346-
yyjson_mut_arr_add_val(row, av);
6347-
} else {
6348-
yyjson_mut_arr_add_str(doc, row, "");
6349-
}
6350-
} else {
6351-
yyjson_mut_arr_add_str(doc, row, "");
6352-
}
6353-
}
6365+
/* Emit in header order: strategy, confidence, then args. Swapping these
6366+
* two blocks mislabeled every include_evidence+data_flow json row
6367+
* (#1542 leftover). */
63546368
if (include_evidence) {
63556369
const char *ev_class = NULL;
63566370
double ev_conf = -1.0;
@@ -6370,6 +6384,20 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
63706384
yyjson_mut_arr_add_null(doc, row);
63716385
}
63726386
}
6387+
if (data_flow) {
6388+
size_t alen = 0;
6389+
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
6390+
if (ea && alen > 0) {
6391+
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
6392+
if (av) {
6393+
yyjson_mut_arr_add_val(row, av);
6394+
} else {
6395+
yyjson_mut_arr_add_str(doc, row, "");
6396+
}
6397+
} else {
6398+
yyjson_mut_arr_add_str(doc, row, "");
6399+
}
6400+
}
63736401
yyjson_mut_arr_add_val(cur_rows, row);
63746402
}
63756403
yyjson_mut_obj_add_val(doc, leg, "groups", groups);
@@ -6782,15 +6810,17 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
67826810
if (do_outbound) {
67836811
cbm_tree_scalar_int(&sb, "callees_total", out_total);
67846812
if (flat_trace) {
6785-
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow);
6813+
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow,
6814+
include_evidence);
67866815
} else {
67876816
bfs_to_tree_table(&sb, "callees", &view_out, include_tests, include_evidence);
67886817
}
67896818
}
67906819
if (do_inbound) {
67916820
cbm_tree_scalar_int(&sb, "callers_total", in_total);
67926821
if (flat_trace) {
6793-
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow);
6822+
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow,
6823+
include_evidence);
67946824
} else {
67956825
bfs_to_tree_table(&sb, "callers", &view_in, include_tests, include_evidence);
67966826
}

‎tests/test_mcp.c‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3385,6 +3385,118 @@ TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped) {
33853385
PASS();
33863386
}
33873387

3388+
/* #1542 leftover: header order is strategy,confidence then args, but json
3389+
* used to emit args first; tree flat_trace (risk_labels || data_flow) used
3390+
* to call bfs_to_toon_table without include_evidence. Pin both: every row
3391+
* has len(cols)==len(row), and the strategy cell is the class not the args
3392+
* array. */
3393+
TEST(tool_trace_path_evidence_columns_match_header_issue1542) {
3394+
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
3395+
cbm_store_t *st = cbm_mcp_server_store(srv);
3396+
const char *proj = "ev-order";
3397+
cbm_mcp_server_set_project(srv, proj);
3398+
cbm_store_upsert_project(st, proj, "/tmp/ev-order");
3399+
cbm_node_t caller = {.project = proj,
3400+
.label = "Function",
3401+
.name = "caller",
3402+
.qualified_name = "ev-order.src.caller",
3403+
.file_path = "src/a.c",
3404+
.start_line = 1,
3405+
.end_line = 5};
3406+
cbm_node_t callee = {.project = proj,
3407+
.label = "Function",
3408+
.name = "target",
3409+
.qualified_name = "ev-order.src.target",
3410+
.file_path = "src/a.c",
3411+
.start_line = 10,
3412+
.end_line = 20};
3413+
int64_t id_caller = cbm_store_upsert_node(st, &caller);
3414+
int64_t id_callee = cbm_store_upsert_node(st, &callee);
3415+
ASSERT_GT(id_caller, 0);
3416+
ASSERT_GT(id_callee, 0);
3417+
cbm_edge_t e = {.project = proj,
3418+
.source_id = id_caller,
3419+
.target_id = id_callee,
3420+
.type = "CALLS",
3421+
.properties_json = "{\"callee\":\"target\",\"confidence\":0.95,"
3422+
"\"strategy\":\"lsp_trait_dispatch\",\"candidates\":1,"
3423+
"\"args\":[\"x\"]}"};
3424+
ASSERT_GT(cbm_store_insert_edge(st, &e), 0);
3425+
3426+
/* json × data_flow × include_evidence: cols identity, not just count. */
3427+
char *js = cbm_mcp_server_handle(
3428+
srv, "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"tools/call\","
3429+
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3430+
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3431+
"\"mode\":\"data_flow\",\"format\":\"json\"}}}");
3432+
ASSERT_NOT_NULL(js);
3433+
char *js_txt = extract_text_content(js);
3434+
ASSERT_NOT_NULL(js_txt);
3435+
yyjson_doc *doc = yyjson_read(js_txt, strlen(js_txt), 0);
3436+
ASSERT_NOT_NULL(doc);
3437+
yyjson_val *callees = yyjson_obj_get(yyjson_doc_get_root(doc), "callees");
3438+
ASSERT_NOT_NULL(callees);
3439+
yyjson_val *cols = yyjson_obj_get(callees, "cols");
3440+
ASSERT_NOT_NULL(cols);
3441+
static const char *want[] = {"name", "hop", "strategy", "confidence", "args"};
3442+
ASSERT_EQ((int)yyjson_arr_size(cols), 5);
3443+
for (int i = 0; i < 5; i++) {
3444+
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(cols, i)), want[i]);
3445+
}
3446+
yyjson_val *hop1 = NULL;
3447+
yyjson_val *groups = yyjson_obj_get(callees, "groups");
3448+
ASSERT_NOT_NULL(groups);
3449+
size_t ng = yyjson_arr_size(groups);
3450+
for (size_t g = 0; g < ng; g++) {
3451+
yyjson_val *rows = yyjson_obj_get(yyjson_arr_get(groups, g), "rows");
3452+
if (!rows) {
3453+
continue;
3454+
}
3455+
size_t nr = yyjson_arr_size(rows);
3456+
for (size_t r = 0; r < nr; r++) {
3457+
yyjson_val *row = yyjson_arr_get(rows, r);
3458+
yyjson_val *hop = row ? yyjson_arr_get(row, 1) : NULL;
3459+
if (hop && yyjson_get_int(hop) >= 1) {
3460+
hop1 = row;
3461+
break;
3462+
}
3463+
}
3464+
if (hop1) {
3465+
break;
3466+
}
3467+
}
3468+
ASSERT_NOT_NULL(hop1);
3469+
ASSERT_EQ((int)yyjson_arr_size(hop1), 5);
3470+
ASSERT_TRUE(yyjson_is_str(yyjson_arr_get(hop1, 2)));
3471+
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(hop1, 2)), "lsp");
3472+
ASSERT_TRUE(yyjson_is_num(yyjson_arr_get(hop1, 3)));
3473+
ASSERT_TRUE(yyjson_is_arr(yyjson_arr_get(hop1, 4)));
3474+
yyjson_doc_free(doc);
3475+
free(js_txt);
3476+
free(js);
3477+
3478+
/* tree × risk_labels × include_evidence used to drop evidence entirely
3479+
* because flat_trace routed through bfs_to_toon_table without the flag. */
3480+
char *tree = cbm_mcp_server_handle(
3481+
srv, "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\","
3482+
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3483+
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3484+
"\"risk_labels\":true}}}");
3485+
ASSERT_NOT_NULL(tree);
3486+
char *tree_txt = extract_text_content(tree);
3487+
ASSERT_NOT_NULL(tree_txt);
3488+
ASSERT_NOT_NULL(strstr(tree_txt, "strategy"));
3489+
ASSERT_NOT_NULL(strstr(tree_txt, "confidence"));
3490+
ASSERT_NOT_NULL(strstr(tree_txt, "lsp"));
3491+
ASSERT_NOT_NULL(strstr(tree_txt, "0.95"));
3492+
ASSERT_NULL(strstr(tree_txt, "lsp_trait_dispatch"));
3493+
free(tree_txt);
3494+
free(tree);
3495+
3496+
cbm_mcp_server_free(srv);
3497+
PASS();
3498+
}
3499+
33883500
/* Reproduce-first (#887): the client-supplied `depth` on trace_call_path must be
33893501
* clamped to the MCP ceiling (cbm_mcp_max_depth(), default 15). On origin/main
33903502
* an MCP_MAX_DEPTH=15 constant was defined but never applied — `depth` flowed
@@ -11188,6 +11300,7 @@ SUITE(mcp) {
1118811300
RUN_TEST(tool_trace_call_path_prefers_definition);
1118911301
RUN_TEST(trace_evidence_strategy_class_vocabulary_is_closed);
1119011302
RUN_TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped);
11303+
RUN_TEST(tool_trace_path_evidence_columns_match_header_issue1542);
1119111304
RUN_TEST(tool_trace_call_path_depth_clamped);
1119211305
RUN_TEST(tool_trace_call_path_distinct_defs_not_over_unioned);
1119311306
RUN_TEST(tool_trace_call_path_dts_stub_unions_with_impl);

0 commit comments

Comments
 (0)