mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge origin/main into feat/codex-skill-location-compat
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
01ac4fe6a9
3 files changed
+289
-1
No files matched your search
@@ -963,6 +963,16 @@ mode. `index_status` keeps describing the published graph and its freshness.
|
||||
|
||||
`manage_adr` query modes (`get` and `sections`) use the server's cached query store so they can proceed while a same-project reindex is running. If another process publishes a replacement store during reindexing, they can return the pre-publication ADR until idle eviction refreshes that cache. Updates remain serialized through the project mutation guard.
|
||||
|
||||
`trace_path` exposes the start nodes' stored `self_recursive` (direct recursion)
|
||||
and `recursive` (direct recursion or a call-graph cycle) signals in both tree and
|
||||
JSON output. Across multiple start nodes, a flag is `true` if any records true,
|
||||
`false` if all record false, and `null` otherwise (including missing legacy
|
||||
metadata). These are static-analysis signals, not counts of resolved self-calls.
|
||||
Static `CALLS` indexing omits direct self-call edges; traces that include `CALLS` state
|
||||
`self_call_policy: omitted`. Caller/callee totals still count graph-reachable
|
||||
nodes, so a recursive function can have zero callees. Recursion metadata remains
|
||||
visible on continuation pages and when an output budget prevents rows from fitting.
|
||||
|
||||
## Graph Data Model
|
||||
|
||||
### Node Labels
|
||||
|
||||
@@ -10368,6 +10368,72 @@ static void trace_call_coverage(cbm_store_t *store, const char *project,
|
||||
cbm_ht_free(match.targets);
|
||||
}
|
||||
|
||||
enum { TRACE_RECURSION_FLAG_COUNT = 2 };
|
||||
typedef enum {
|
||||
TRACE_RECURSION_UNKNOWN = -1,
|
||||
TRACE_RECURSION_FALSE,
|
||||
TRACE_RECURSION_TRUE,
|
||||
} trace_recursion_flag_t;
|
||||
static const char *const trace_recursion_keys[TRACE_RECURSION_FLAG_COUNT] = {"self_recursive",
|
||||
"recursive"};
|
||||
typedef struct {
|
||||
/* -1: unknown, 0: all roots record false, 1: at least one records true. */
|
||||
trace_recursion_flag_t flags[TRACE_RECURSION_FLAG_COUNT];
|
||||
} trace_recursion_t;
|
||||
|
||||
static trace_recursion_t trace_recursion_metadata(const cbm_node_t *nodes, int node_count) {
|
||||
trace_recursion_t metadata = {0};
|
||||
/* Use the same roots as bfs_union_same_name, rather than borrowing a
|
||||
* callee's flag or selecting just one definition/stub. These are stored
|
||||
* analysis signals, not evidence that a resolved self-call was dropped. */
|
||||
for (int i = 0; i < node_count; i++) {
|
||||
const char *properties = nodes[i].properties_json;
|
||||
yyjson_doc *doc = properties ? yyjson_read(properties, strlen(properties), 0) : NULL;
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
for (int j = 0; j < TRACE_RECURSION_FLAG_COUNT; j++) {
|
||||
yyjson_val *value = yyjson_obj_get(root, trace_recursion_keys[j]);
|
||||
if (yyjson_is_bool(value) && yyjson_get_bool(value)) {
|
||||
metadata.flags[j] = TRACE_RECURSION_TRUE;
|
||||
} else if (!yyjson_is_bool(value) && metadata.flags[j] != TRACE_RECURSION_TRUE) {
|
||||
metadata.flags[j] = TRACE_RECURSION_UNKNOWN;
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
static void trace_recursion_tree(cbm_sb_t *sb, const trace_recursion_t *metadata,
|
||||
bool includes_calls) {
|
||||
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
|
||||
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
|
||||
cbm_sb_append(sb, trace_recursion_keys[i]);
|
||||
cbm_sb_append(sb, ": null\n");
|
||||
} else {
|
||||
cbm_tree_scalar_bool(sb, trace_recursion_keys[i],
|
||||
metadata->flags[i] == TRACE_RECURSION_TRUE);
|
||||
}
|
||||
}
|
||||
if (includes_calls) {
|
||||
cbm_tree_scalar_str(sb, "self_call_policy", "omitted");
|
||||
}
|
||||
}
|
||||
|
||||
static void trace_recursion_json(yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
const trace_recursion_t *metadata, bool includes_calls) {
|
||||
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
|
||||
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
|
||||
yyjson_mut_obj_add_null(doc, root, trace_recursion_keys[i]);
|
||||
} else {
|
||||
yyjson_mut_obj_add_bool(doc, root, trace_recursion_keys[i],
|
||||
metadata->flags[i] == TRACE_RECURSION_TRUE);
|
||||
}
|
||||
}
|
||||
if (includes_calls) {
|
||||
yyjson_mut_obj_add_str(doc, root, "self_call_policy", "omitted");
|
||||
}
|
||||
}
|
||||
|
||||
static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
|
||||
char *project = get_project_arg(args);
|
||||
@@ -10722,6 +10788,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
.do_inbound = do_inbound};
|
||||
trace_call_coverage(store, project, &coverage_scope, trace_includes_calls, &unresolved_out,
|
||||
&unresolved_in);
|
||||
trace_recursion_t recursion = trace_recursion_metadata(nodes, node_count);
|
||||
const char *out_relation = unresolved_out ? "unknown" : (tr_out.truncated ? "gte" : "eq");
|
||||
const char *in_relation = unresolved_in ? "unknown" : (tr_in.truncated ? "gte" : "eq");
|
||||
|
||||
@@ -10805,6 +10872,7 @@ render_trace_output:;
|
||||
if (mode) {
|
||||
cbm_tree_scalar_str(&sb, "mode", mode);
|
||||
}
|
||||
trace_recursion_tree(&sb, &recursion, trace_includes_calls);
|
||||
/* Grouped tree is THE default; risk_labels/data_flow keep the flat
|
||||
* table (extra columns) in the same tree syntax. */
|
||||
bool render_risk = risk_labels && emit_optional_fields;
|
||||
@@ -10888,6 +10956,7 @@ render_trace_output:;
|
||||
if (mode) {
|
||||
yyjson_mut_obj_add_str(doc, root, "mode", mode);
|
||||
}
|
||||
trace_recursion_json(doc, root, &recursion, trace_includes_calls);
|
||||
if (do_outbound) {
|
||||
yyjson_mut_obj_add_int(doc, root, "callees_total", out_total);
|
||||
yyjson_mut_obj_add_str(doc, root, "callees_total_relation", out_relation);
|
||||
@@ -10955,6 +11024,7 @@ render_trace_output:;
|
||||
if (!trace_legacy_json) {
|
||||
cbm_sb_t floor;
|
||||
cbm_sb_init(&floor);
|
||||
trace_recursion_tree(&floor, &recursion, trace_includes_calls);
|
||||
if (do_outbound) {
|
||||
cbm_tree_scalar_int(&floor, "callees_total", out_total);
|
||||
cbm_tree_scalar_str(&floor, "callees_total_relation", out_relation);
|
||||
@@ -10983,6 +11053,7 @@ render_trace_output:;
|
||||
yyjson_mut_doc *floor_doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *floor = yyjson_mut_obj(floor_doc);
|
||||
yyjson_mut_doc_set_root(floor_doc, floor);
|
||||
trace_recursion_json(floor_doc, floor, &recursion, trace_includes_calls);
|
||||
if (do_outbound) {
|
||||
yyjson_mut_obj_add_int(floor_doc, floor, "callees_total", out_total);
|
||||
yyjson_mut_obj_add_str(floor_doc, floor, "callees_total_relation",
|
||||
|
||||
+208
-1
@@ -7843,6 +7843,200 @@ TEST(tool_index_status_includes_git_metadata) {
|
||||
* TOOL HANDLERS WITH DATA
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* #2816: stored recursion must remain visible even when the CALLS graph has
|
||||
* no self-edges. Removing metadata emission must fail these assertions. */
|
||||
TEST(tool_trace_path_recursion_metadata_issue2816) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
const char *proj = "recursion-proj";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion"), CBM_STORE_OK);
|
||||
const struct {
|
||||
const char *name;
|
||||
const char *props;
|
||||
const char *self;
|
||||
const char *recursive;
|
||||
} cases[] = {
|
||||
{"countdown", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
|
||||
{"visit", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
|
||||
{"mutual", "{\"self_recursive\":false,\"recursive\":true}", "false", "true"},
|
||||
{"leaf", "{\"self_recursive\":false,\"recursive\":false}", "false", "false"},
|
||||
{"legacy", "{}", "null", "null"},
|
||||
{"partial", "{\"self_recursive\":true}", "true", "null"},
|
||||
{"invalid", "{\"self_recursive\":\"true\",\"recursive\":1}", "null", "null"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
char qn[128];
|
||||
snprintf(qn, sizeof(qn), "%s.%s", proj, cases[i].name);
|
||||
cbm_node_t n = {.project = proj,
|
||||
.label = i == 1 ? "Method" : "Function",
|
||||
.name = cases[i].name,
|
||||
.qualified_name = qn,
|
||||
.file_path = "recursion.ts",
|
||||
.start_line = 1,
|
||||
.end_line = 5,
|
||||
.properties_json = cases[i].props};
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &n), 0);
|
||||
for (int json = 0; json < 2; json++) {
|
||||
for (int inbound = 0; inbound < 2; inbound++) {
|
||||
char args[512];
|
||||
snprintf(args, sizeof(args),
|
||||
"{\"project\":\"%s\",\"function_name\":\"%s\","
|
||||
"\"direction\":\"%s\",\"format\":\"%s\"}",
|
||||
proj, cases[i].name, inbound ? "inbound" : "outbound",
|
||||
json ? "json" : "tree");
|
||||
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
|
||||
char *inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
char expected[128];
|
||||
snprintf(expected, sizeof(expected),
|
||||
json ? "\"self_recursive\":%s" : "self_recursive: %s\n", cases[i].self);
|
||||
ASSERT_NOT_NULL(strstr(inner, expected));
|
||||
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
|
||||
cases[i].recursive);
|
||||
ASSERT_NOT_NULL(strstr(inner, expected));
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
|
||||
: "self_call_policy: omitted\n"));
|
||||
snprintf(expected, sizeof(expected), json ? "\"%s_total\":0" : "%s_total: 0\n",
|
||||
inbound ? "callers" : "callees");
|
||||
ASSERT_NOT_NULL(strstr(inner, expected));
|
||||
free(inner);
|
||||
free(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_trace_path_recursion_union_and_edges_issue2816) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
const char *proj = "recursion-union";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-union"), CBM_STORE_OK);
|
||||
cbm_node_t def = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "countdown",
|
||||
.qualified_name = "recursion-union.impl.countdown",
|
||||
.file_path = "impl.ts",
|
||||
.start_line = 1,
|
||||
.end_line = 10,
|
||||
.properties_json = "{\"self_recursive\":false,\"recursive\":false}"};
|
||||
int64_t def_id = cbm_store_upsert_node(st, &def);
|
||||
ASSERT_GT(def_id, 0);
|
||||
cbm_node_t stub = def;
|
||||
stub.qualified_name = "recursion-union.stub.countdown";
|
||||
stub.file_path = "stub.d.ts";
|
||||
stub.end_line = 1;
|
||||
stub.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
|
||||
cbm_node_t leaf = def;
|
||||
leaf.name = "leaf";
|
||||
leaf.qualified_name = "recursion-union.impl.leaf";
|
||||
leaf.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
|
||||
int64_t leaf_id = cbm_store_upsert_node(st, &leaf);
|
||||
ASSERT_GT(leaf_id, 0);
|
||||
cbm_edge_t edge = {.project = proj, .source_id = def_id, .target_id = leaf_id, .type = "CALLS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
|
||||
const char *expected_flags[] = {"true", "null", "false", "true"};
|
||||
for (int state = 0; state < 4; state++) {
|
||||
def.properties_json = state == 3 ? "{\"self_recursive\":true,\"recursive\":true}"
|
||||
: "{\"self_recursive\":false,\"recursive\":false}";
|
||||
stub.properties_json = state == 0 ? "{\"self_recursive\":true,\"recursive\":true}"
|
||||
: state == 2 ? "{\"self_recursive\":false,\"recursive\":false}"
|
||||
: "{}";
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &def), 0);
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
|
||||
for (int json = 0; json < 2; json++) {
|
||||
char args[512];
|
||||
snprintf(args, sizeof(args),
|
||||
"{\"project\":\"%s\",\"function_name\":\"countdown\","
|
||||
"\"direction\":\"outbound\",\"format\":\"%s\"}",
|
||||
proj, json ? "json" : "tree");
|
||||
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
|
||||
char *inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"callees_total\":1" : "callees_total: 1\n"));
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"leaf\"" : ".leaf"));
|
||||
char expected[128];
|
||||
snprintf(expected, sizeof(expected),
|
||||
json ? "\"self_recursive\":%s" : "self_recursive: %s\n",
|
||||
expected_flags[state]);
|
||||
ASSERT_NOT_NULL(strstr(inner, expected));
|
||||
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
|
||||
expected_flags[state]);
|
||||
ASSERT_NOT_NULL(strstr(inner, expected));
|
||||
free(inner);
|
||||
free(response);
|
||||
}
|
||||
}
|
||||
/* Recursion is root metadata, but the omission policy concerns CALLS only. */
|
||||
char *response =
|
||||
cbm_mcp_handle_tool(srv, "trace_path",
|
||||
"{\"project\":\"recursion-union\",\"function_name\":\"countdown\","
|
||||
"\"edge_types\":[\"IMPORTS\"],\"format\":\"json\"}");
|
||||
char *inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
|
||||
ASSERT_NULL(strstr(inner, "self_call_policy"));
|
||||
free(inner);
|
||||
free(response);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_trace_path_recursion_survives_budget_floor_issue2816) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
const char *proj = "recursion-budget";
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-budget"), CBM_STORE_OK);
|
||||
char huge[4096];
|
||||
memset(huge, 'x', sizeof(huge) - 1);
|
||||
huge[sizeof(huge) - 1] = '\0';
|
||||
cbm_node_t n = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "countdown",
|
||||
.qualified_name = "recursion-budget.countdown",
|
||||
.file_path = "recursion.ts",
|
||||
.start_line = 1,
|
||||
.end_line = 10,
|
||||
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
|
||||
int64_t root_id = cbm_store_upsert_node(st, &n);
|
||||
ASSERT_GT(root_id, 0);
|
||||
n.name = huge;
|
||||
n.qualified_name = huge;
|
||||
int64_t callee_id = cbm_store_upsert_node(st, &n);
|
||||
ASSERT_GT(callee_id, 0);
|
||||
cbm_edge_t edge = {
|
||||
.project = proj, .source_id = root_id, .target_id = callee_id, .type = "CALLS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
|
||||
for (int json = 0; json < 2; json++) {
|
||||
char args[512];
|
||||
snprintf(args, sizeof(args),
|
||||
"{\"project\":\"%s\",\"function_name\":\"countdown\","
|
||||
"\"direction\":\"outbound\",\"format\":\"%s\",\"max_output_tokens\":128}",
|
||||
proj, json ? "json" : "tree");
|
||||
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
|
||||
char *inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "output_budget"));
|
||||
ASSERT_NULL(strstr(inner, huge));
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_recursive\":true" : "self_recursive: true\n"));
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"recursive\":true" : "recursive: true\n"));
|
||||
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
|
||||
: "self_call_policy: omitted\n"));
|
||||
free(inner);
|
||||
free(response);
|
||||
}
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_trace_call_path_not_found) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
|
||||
@@ -8540,7 +8734,8 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
|
||||
.qualified_name = "trace-budget-cursor.hub",
|
||||
.file_path = "src/hub.c",
|
||||
.start_line = 1,
|
||||
.end_line = 2};
|
||||
.end_line = 2,
|
||||
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
|
||||
cbm_node_t short_node = {.project = project,
|
||||
.label = "Function",
|
||||
.name = "short_row",
|
||||
@@ -8580,6 +8775,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
|
||||
"\"format\":\"json\"}");
|
||||
char *inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
|
||||
ASSERT_NOT_NULL(strstr(inner, "short_row"));
|
||||
yyjson_doc *doc = yyjson_read(inner, strlen(inner), 0);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
@@ -8600,6 +8798,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
|
||||
response = cbm_mcp_handle_tool(srv, "trace_path", args);
|
||||
inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
|
||||
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
|
||||
ASSERT_NULL(strstr(inner, long_qn));
|
||||
doc = yyjson_read(inner, strlen(inner), 0);
|
||||
@@ -8619,6 +8820,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
|
||||
response = cbm_mcp_handle_tool(srv, "trace_path", args);
|
||||
inner = extract_text_content(response);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
|
||||
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
|
||||
ASSERT_NULL(strstr(inner, "short_row"));
|
||||
doc = yyjson_read(inner, strlen(inner), 0);
|
||||
@@ -26697,6 +26901,9 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_index_status_includes_git_metadata);
|
||||
|
||||
/* Tool handlers with validation */
|
||||
RUN_TEST(tool_trace_path_recursion_metadata_issue2816);
|
||||
RUN_TEST(tool_trace_path_recursion_union_and_edges_issue2816);
|
||||
RUN_TEST(tool_trace_path_recursion_survives_budget_floor_issue2816);
|
||||
RUN_TEST(tool_trace_call_path_not_found);
|
||||
RUN_TEST(tool_call_invalid_project_name_leaves_no_corrupt_litter_issue1425);
|
||||
RUN_TEST(tool_trace_missing_function_name);
|
||||
|
||||
Reference in new issue
Block a user