mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-31 07:33:32 +08:00
Merge pull request #1703 from rudi193-cmd/fix/1542-trace-json-evidence
fix(mcp): emit trace evidence columns in header order
This commit is contained in:
+50
-20
@@ -6507,10 +6507,13 @@ static bool bfs_edge_evidence_for_hop(cbm_traverse_result_t *tr, int64_t hop_nod
|
||||
}
|
||||
|
||||
/* TOON table for one trace direction: callees[N]{qn,hop,...} with optional
|
||||
* risk / test / args columns. `name` is omitted (it is the qn's last
|
||||
* segment); the per-item JSON key envelope was 84% of the legacy payload. */
|
||||
* risk / test / evidence / args columns. `name` is omitted (it is the qn's
|
||||
* last segment); the per-item JSON key envelope was 84% of the legacy
|
||||
* payload. include_evidence must be forwarded here — flat_trace used to
|
||||
* drop it (#1542 leftover). */
|
||||
static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result_t *tr,
|
||||
bool risk_labels, bool include_tests, bool data_flow) {
|
||||
bool risk_labels, bool include_tests, bool data_flow,
|
||||
bool include_evidence) {
|
||||
int visible = 0;
|
||||
for (int i = 0; i < tr->visited_count; i++) {
|
||||
if (!include_tests && is_test_file(tr->visited[i].node.file_path)) {
|
||||
@@ -6518,7 +6521,8 @@ static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
|
||||
}
|
||||
visible++;
|
||||
}
|
||||
const char *cols[5] = {"qn", "hop"};
|
||||
/* Max: qn hop risk test strategy confidence args. */
|
||||
const char *cols[7] = {"qn", "hop"};
|
||||
int ncols = 2;
|
||||
if (risk_labels) {
|
||||
cols[ncols++] = "risk";
|
||||
@@ -6526,6 +6530,10 @@ static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
|
||||
if (include_tests) {
|
||||
cols[ncols++] = "test";
|
||||
}
|
||||
if (include_evidence) {
|
||||
cols[ncols++] = "strategy";
|
||||
cols[ncols++] = "confidence";
|
||||
}
|
||||
if (data_flow) {
|
||||
cols[ncols++] = "args";
|
||||
}
|
||||
@@ -6545,6 +6553,23 @@ static void bfs_to_toon_table(cbm_sb_t *sb, const char *key, cbm_traverse_result
|
||||
if (include_tests) {
|
||||
cbm_tree_cell_bool(sb, test, false);
|
||||
}
|
||||
if (include_evidence) {
|
||||
const char *ev_class = NULL;
|
||||
double ev_conf = -1.0;
|
||||
if (bfs_edge_evidence_for_hop(tr, tr->visited[i].node.id, &ev_class, &ev_conf)) {
|
||||
cbm_tree_cell_str(sb, ev_class ? ev_class : "", false);
|
||||
if (ev_conf >= 0.0) {
|
||||
cbm_tree_cell_real(sb, ev_conf, false);
|
||||
} else {
|
||||
cbm_tree_cell_str(sb, "-", false);
|
||||
}
|
||||
} else {
|
||||
/* Root hop / non-CALLS: keep column count fixed, same "-"
|
||||
* placeholders as bfs_to_tree_table. */
|
||||
cbm_tree_cell_str(sb, "-", false);
|
||||
cbm_tree_cell_str(sb, "-", false);
|
||||
}
|
||||
}
|
||||
if (data_flow) {
|
||||
size_t alen = 0;
|
||||
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
|
||||
if (risk_labels) {
|
||||
yyjson_mut_arr_add_str(doc, row, cbm_risk_label(cbm_hop_to_risk(tr->visited[i].hop)));
|
||||
}
|
||||
if (data_flow) {
|
||||
size_t alen = 0;
|
||||
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
|
||||
if (ea && alen > 0) {
|
||||
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
|
||||
if (av) {
|
||||
yyjson_mut_arr_add_val(row, av);
|
||||
} else {
|
||||
yyjson_mut_arr_add_str(doc, row, "");
|
||||
}
|
||||
} else {
|
||||
yyjson_mut_arr_add_str(doc, row, "");
|
||||
}
|
||||
}
|
||||
/* Emit in header order: strategy, confidence, then args. Swapping these
|
||||
* two blocks mislabeled every include_evidence+data_flow json row
|
||||
* (#1542 leftover). */
|
||||
if (include_evidence) {
|
||||
const char *ev_class = NULL;
|
||||
double ev_conf = -1.0;
|
||||
@@ -6923,6 +6937,20 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result
|
||||
yyjson_mut_arr_add_null(doc, row);
|
||||
}
|
||||
}
|
||||
if (data_flow) {
|
||||
size_t alen = 0;
|
||||
const char *ea = bfs_edge_args_for_hop(tr, tr->visited[i].node.id, &alen);
|
||||
if (ea && alen > 0) {
|
||||
yyjson_mut_val *av = yyjson_mut_rawn(doc, ea, alen);
|
||||
if (av) {
|
||||
yyjson_mut_arr_add_val(row, av);
|
||||
} else {
|
||||
yyjson_mut_arr_add_str(doc, row, "");
|
||||
}
|
||||
} else {
|
||||
yyjson_mut_arr_add_str(doc, row, "");
|
||||
}
|
||||
}
|
||||
yyjson_mut_arr_add_val(cur_rows, row);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, leg, "groups", groups);
|
||||
@@ -7335,7 +7363,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (do_outbound) {
|
||||
cbm_tree_scalar_int(&sb, "callees_total", out_total);
|
||||
if (flat_trace) {
|
||||
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow);
|
||||
bfs_to_toon_table(&sb, "callees", &view_out, risk_labels, include_tests, data_flow,
|
||||
include_evidence);
|
||||
} else {
|
||||
bfs_to_tree_table(&sb, "callees", &view_out, include_tests, include_evidence);
|
||||
}
|
||||
@@ -7343,7 +7372,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (do_inbound) {
|
||||
cbm_tree_scalar_int(&sb, "callers_total", in_total);
|
||||
if (flat_trace) {
|
||||
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow);
|
||||
bfs_to_toon_table(&sb, "callers", &view_in, risk_labels, include_tests, data_flow,
|
||||
include_evidence);
|
||||
} else {
|
||||
bfs_to_tree_table(&sb, "callers", &view_in, include_tests, include_evidence);
|
||||
}
|
||||
|
||||
@@ -3725,6 +3725,118 @@ TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1542 leftover: header order is strategy,confidence then args, but json
|
||||
* used to emit args first; tree flat_trace (risk_labels || data_flow) used
|
||||
* to call bfs_to_toon_table without include_evidence. Pin both: every row
|
||||
* has len(cols)==len(row), and the strategy cell is the class not the args
|
||||
* array. */
|
||||
TEST(tool_trace_path_evidence_columns_match_header_issue1542) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
const char *proj = "ev-order";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
cbm_store_upsert_project(st, proj, "/tmp/ev-order");
|
||||
cbm_node_t caller = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "caller",
|
||||
.qualified_name = "ev-order.src.caller",
|
||||
.file_path = "src/a.c",
|
||||
.start_line = 1,
|
||||
.end_line = 5};
|
||||
cbm_node_t callee = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "target",
|
||||
.qualified_name = "ev-order.src.target",
|
||||
.file_path = "src/a.c",
|
||||
.start_line = 10,
|
||||
.end_line = 20};
|
||||
int64_t id_caller = cbm_store_upsert_node(st, &caller);
|
||||
int64_t id_callee = cbm_store_upsert_node(st, &callee);
|
||||
ASSERT_GT(id_caller, 0);
|
||||
ASSERT_GT(id_callee, 0);
|
||||
cbm_edge_t e = {.project = proj,
|
||||
.source_id = id_caller,
|
||||
.target_id = id_callee,
|
||||
.type = "CALLS",
|
||||
.properties_json = "{\"callee\":\"target\",\"confidence\":0.95,"
|
||||
"\"strategy\":\"lsp_trait_dispatch\",\"candidates\":1,"
|
||||
"\"args\":[\"x\"]}"};
|
||||
ASSERT_GT(cbm_store_insert_edge(st, &e), 0);
|
||||
|
||||
/* json × data_flow × include_evidence: cols identity, not just count. */
|
||||
char *js = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
|
||||
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
|
||||
"\"mode\":\"data_flow\",\"format\":\"json\"}}}");
|
||||
ASSERT_NOT_NULL(js);
|
||||
char *js_txt = extract_text_content(js);
|
||||
ASSERT_NOT_NULL(js_txt);
|
||||
yyjson_doc *doc = yyjson_read(js_txt, strlen(js_txt), 0);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
yyjson_val *callees = yyjson_obj_get(yyjson_doc_get_root(doc), "callees");
|
||||
ASSERT_NOT_NULL(callees);
|
||||
yyjson_val *cols = yyjson_obj_get(callees, "cols");
|
||||
ASSERT_NOT_NULL(cols);
|
||||
static const char *want[] = {"name", "hop", "strategy", "confidence", "args"};
|
||||
ASSERT_EQ((int)yyjson_arr_size(cols), 5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(cols, i)), want[i]);
|
||||
}
|
||||
yyjson_val *hop1 = NULL;
|
||||
yyjson_val *groups = yyjson_obj_get(callees, "groups");
|
||||
ASSERT_NOT_NULL(groups);
|
||||
size_t ng = yyjson_arr_size(groups);
|
||||
for (size_t g = 0; g < ng; g++) {
|
||||
yyjson_val *rows = yyjson_obj_get(yyjson_arr_get(groups, g), "rows");
|
||||
if (!rows) {
|
||||
continue;
|
||||
}
|
||||
size_t nr = yyjson_arr_size(rows);
|
||||
for (size_t r = 0; r < nr; r++) {
|
||||
yyjson_val *row = yyjson_arr_get(rows, r);
|
||||
yyjson_val *hop = row ? yyjson_arr_get(row, 1) : NULL;
|
||||
if (hop && yyjson_get_int(hop) >= 1) {
|
||||
hop1 = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hop1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(hop1);
|
||||
ASSERT_EQ((int)yyjson_arr_size(hop1), 5);
|
||||
ASSERT_TRUE(yyjson_is_str(yyjson_arr_get(hop1, 2)));
|
||||
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(hop1, 2)), "lsp");
|
||||
ASSERT_TRUE(yyjson_is_num(yyjson_arr_get(hop1, 3)));
|
||||
ASSERT_TRUE(yyjson_is_arr(yyjson_arr_get(hop1, 4)));
|
||||
yyjson_doc_free(doc);
|
||||
free(js_txt);
|
||||
free(js);
|
||||
|
||||
/* tree × risk_labels × include_evidence used to drop evidence entirely
|
||||
* because flat_trace routed through bfs_to_toon_table without the flag. */
|
||||
char *tree = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
|
||||
"\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
|
||||
"\"risk_labels\":true}}}");
|
||||
ASSERT_NOT_NULL(tree);
|
||||
char *tree_txt = extract_text_content(tree);
|
||||
ASSERT_NOT_NULL(tree_txt);
|
||||
ASSERT_NOT_NULL(strstr(tree_txt, "strategy"));
|
||||
ASSERT_NOT_NULL(strstr(tree_txt, "confidence"));
|
||||
ASSERT_NOT_NULL(strstr(tree_txt, "lsp"));
|
||||
ASSERT_NOT_NULL(strstr(tree_txt, "0.95"));
|
||||
ASSERT_NULL(strstr(tree_txt, "lsp_trait_dispatch"));
|
||||
free(tree_txt);
|
||||
free(tree);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Reproduce-first (#887): the client-supplied `depth` on trace_call_path must be
|
||||
* clamped to the MCP ceiling (cbm_mcp_max_depth(), default 15). On origin/main
|
||||
* an MCP_MAX_DEPTH=15 constant was defined but never applied — `depth` flowed
|
||||
@@ -13757,6 +13869,7 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_trace_call_path_prefers_definition);
|
||||
RUN_TEST(trace_evidence_strategy_class_vocabulary_is_closed);
|
||||
RUN_TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped);
|
||||
RUN_TEST(tool_trace_path_evidence_columns_match_header_issue1542);
|
||||
RUN_TEST(tool_trace_call_path_depth_clamped);
|
||||
RUN_TEST(tool_trace_call_path_distinct_defs_not_over_unioned);
|
||||
RUN_TEST(tool_trace_call_path_dts_stub_unions_with_impl);
|
||||
|
||||
Reference in New Issue
Block a user