mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix: count INHERITS/IMPLEMENTS edges in search_graph degree
search_graph reported in_degree/out_degree of 0 for Class nodes that
have real INHERITS edges, because the degree computation in both
cbm_store_node_degree (Cypher virtual properties) and cbm_store_search
(result population and min_degree/max_degree filter) only counted CALLS
(and USAGE) edges. A min_degree filter therefore silently excluded
well-connected classes.
Broaden the edge-type predicate in both surfaces to
IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS') so search_graph and
Cypher report the same edge-type-agnostic degree and stay consistent.
Add regression tests covering: a parent class with N INHERITS children,
combined CALLS+INHERITS degree without double counting, min_degree now
including inheritance-only classes, and isolated nodes still reporting
zero.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
2 files changed
+185
-5
No files matched your search
+6
-4
@@ -1789,7 +1789,8 @@ void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *ou
|
||||
*in_deg = 0;
|
||||
*out_deg = 0;
|
||||
|
||||
const char *in_sql = "SELECT COUNT(*) FROM edges WHERE target_id = ?1 AND type = 'CALLS'";
|
||||
const char *in_sql = "SELECT COUNT(*) FROM edges WHERE target_id = ?1 AND "
|
||||
"type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, in_sql, CBM_NOT_FOUND, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, SKIP_ONE, node_id);
|
||||
@@ -1799,7 +1800,8 @@ void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *ou
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
const char *out_sql = "SELECT COUNT(*) FROM edges WHERE source_id = ?1 AND type = 'CALLS'";
|
||||
const char *out_sql = "SELECT COUNT(*) FROM edges WHERE source_id = ?1 AND "
|
||||
"type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')";
|
||||
if (sqlite3_prepare_v2(s->db, out_sql, CBM_NOT_FOUND, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, SKIP_ONE, node_id);
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -2452,9 +2454,9 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
const char *select_cols = "SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
|
||||
"n.file_path, n.start_line, n.end_line, n.properties, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND "
|
||||
"e.type IN ('CALLS', 'USAGE')) AS in_deg, "
|
||||
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND "
|
||||
"e.type IN ('CALLS', 'USAGE')) AS out_deg ";
|
||||
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS out_deg ";
|
||||
|
||||
char where[CBM_SZ_2K] = "";
|
||||
search_bind_t binds[ST_SEARCH_MAX_BINDS];
|
||||
|
||||
+179
-1
@@ -224,6 +224,15 @@ TEST(store_search_pagination) {
|
||||
|
||||
/* ── Search with degree filter ──────────────────────────────────── */
|
||||
|
||||
static int search_result_index_by_name(const cbm_search_output_t *out, const char *name) {
|
||||
for (int i = 0; i < out->count; i++) {
|
||||
if (out->results[i].node.name && strcmp(out->results[i].node.name, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
TEST(store_search_degree_filter) {
|
||||
int64_t ids[3];
|
||||
cbm_store_t *s = setup_search_store(ids);
|
||||
@@ -239,7 +248,7 @@ TEST(store_search_degree_filter) {
|
||||
ASSERT_EQ(out.count, 2);
|
||||
cbm_store_search_free(&out);
|
||||
|
||||
/* max_degree = 0 should find nodes with no CALLS edges */
|
||||
/* max_degree = 0 should find nodes with no counted degree edges */
|
||||
params.min_degree = -1; /* no min */
|
||||
params.max_degree = 0; /* only zero-degree nodes */
|
||||
params.label = "Function";
|
||||
@@ -253,6 +262,171 @@ TEST(store_search_degree_filter) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_search_degree_counts_inherits) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t parent = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "AbstractAttachmentDto",
|
||||
.qualified_name = "test.AbstractAttachmentDto"};
|
||||
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
||||
ASSERT_GT(parent_id, 0);
|
||||
|
||||
const char *child_names[] = {"AttachmentDtoA", "AttachmentDtoB", "AttachmentDtoC"};
|
||||
const char *child_qns[] = {"test.AttachmentDtoA", "test.AttachmentDtoB",
|
||||
"test.AttachmentDtoC"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
cbm_node_t child = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = child_names[i],
|
||||
.qualified_name = child_qns[i]};
|
||||
int64_t child_id = cbm_store_upsert_node(s, &child);
|
||||
ASSERT_GT(child_id, 0);
|
||||
cbm_edge_t edge = {.project = "test",
|
||||
.source_id = child_id,
|
||||
.target_id = parent_id,
|
||||
.type = "INHERITS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);
|
||||
}
|
||||
|
||||
int in_deg = 0;
|
||||
int out_deg = 0;
|
||||
cbm_store_node_degree(s, parent_id, &in_deg, &out_deg);
|
||||
ASSERT_EQ(in_deg, 3);
|
||||
ASSERT_EQ(out_deg, 0);
|
||||
|
||||
cbm_search_params_t params = {
|
||||
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
||||
cbm_search_output_t out = {0};
|
||||
int rc = cbm_store_search(s, ¶ms, &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
int parent_idx = search_result_index_by_name(&out, "AbstractAttachmentDto");
|
||||
ASSERT_GTE(parent_idx, 0);
|
||||
ASSERT_EQ(out.results[parent_idx].in_degree, 3);
|
||||
ASSERT_EQ(out.results[parent_idx].out_degree, 0);
|
||||
cbm_store_search_free(&out);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_search_degree_calls_plus_inherits_no_double_count) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t child = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "AttachmentDto",
|
||||
.qualified_name = "test.AttachmentDto"};
|
||||
cbm_node_t parent = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "BaseAttachmentDto",
|
||||
.qualified_name = "test.BaseAttachmentDto"};
|
||||
cbm_node_t fn = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "normalizeAttachment",
|
||||
.qualified_name = "test.normalizeAttachment"};
|
||||
int64_t child_id = cbm_store_upsert_node(s, &child);
|
||||
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
||||
int64_t fn_id = cbm_store_upsert_node(s, &fn);
|
||||
ASSERT_GT(child_id, 0);
|
||||
ASSERT_GT(parent_id, 0);
|
||||
ASSERT_GT(fn_id, 0);
|
||||
|
||||
cbm_edge_t calls = {
|
||||
.project = "test", .source_id = child_id, .target_id = fn_id, .type = "CALLS"};
|
||||
cbm_edge_t inherits = {
|
||||
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(s, &calls), 0);
|
||||
ASSERT_GT(cbm_store_insert_edge(s, &inherits), 0);
|
||||
|
||||
int in_deg = 0;
|
||||
int out_deg = 0;
|
||||
cbm_store_node_degree(s, child_id, &in_deg, &out_deg);
|
||||
ASSERT_EQ(in_deg, 0);
|
||||
ASSERT_EQ(out_deg, 2);
|
||||
|
||||
cbm_search_params_t params = {
|
||||
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
||||
cbm_search_output_t out = {0};
|
||||
int rc = cbm_store_search(s, ¶ms, &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
int child_idx = search_result_index_by_name(&out, "AttachmentDto");
|
||||
ASSERT_GTE(child_idx, 0);
|
||||
ASSERT_EQ(out.results[child_idx].in_degree, 0);
|
||||
ASSERT_EQ(out.results[child_idx].out_degree, 2);
|
||||
cbm_store_search_free(&out);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_search_min_degree_includes_inherits_only) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t child = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "InheritanceOnlyChild",
|
||||
.qualified_name = "test.InheritanceOnlyChild"};
|
||||
cbm_node_t parent = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "InheritanceOnlyParent",
|
||||
.qualified_name = "test.InheritanceOnlyParent"};
|
||||
int64_t child_id = cbm_store_upsert_node(s, &child);
|
||||
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
||||
ASSERT_GT(child_id, 0);
|
||||
ASSERT_GT(parent_id, 0);
|
||||
|
||||
cbm_edge_t edge = {
|
||||
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);
|
||||
|
||||
cbm_search_params_t params = {
|
||||
.project = "test", .label = "Class", .min_degree = 1, .max_degree = -1};
|
||||
cbm_search_output_t out = {0};
|
||||
int rc = cbm_store_search(s, ¶ms, &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_GTE(search_result_index_by_name(&out, "InheritanceOnlyParent"), 0);
|
||||
cbm_store_search_free(&out);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_search_isolated_node_zero_degree) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t node = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "LonelyClass",
|
||||
.qualified_name = "test.LonelyClass"};
|
||||
int64_t node_id = cbm_store_upsert_node(s, &node);
|
||||
ASSERT_GT(node_id, 0);
|
||||
|
||||
int in_deg = 0;
|
||||
int out_deg = 0;
|
||||
cbm_store_node_degree(s, node_id, &in_deg, &out_deg);
|
||||
ASSERT_EQ(in_deg, 0);
|
||||
ASSERT_EQ(out_deg, 0);
|
||||
|
||||
cbm_search_params_t params = {
|
||||
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
||||
cbm_search_output_t out = {0};
|
||||
int rc = cbm_store_search(s, ¶ms, &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
int idx = search_result_index_by_name(&out, "LonelyClass");
|
||||
ASSERT_GTE(idx, 0);
|
||||
ASSERT_EQ(out.results[idx].in_degree, 0);
|
||||
ASSERT_EQ(out.results[idx].out_degree, 0);
|
||||
cbm_store_search_free(&out);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Search all (no filters) ────────────────────────────────────── */
|
||||
|
||||
TEST(store_search_all) {
|
||||
@@ -1295,6 +1469,10 @@ SUITE(store_search) {
|
||||
RUN_TEST(store_search_file_pattern_substring_issue200);
|
||||
RUN_TEST(store_search_pagination);
|
||||
RUN_TEST(store_search_degree_filter);
|
||||
RUN_TEST(store_search_degree_counts_inherits);
|
||||
RUN_TEST(store_search_degree_calls_plus_inherits_no_double_count);
|
||||
RUN_TEST(store_search_min_degree_includes_inherits_only);
|
||||
RUN_TEST(store_search_isolated_node_zero_degree);
|
||||
RUN_TEST(store_search_all);
|
||||
RUN_TEST(store_search_exclude_labels);
|
||||
RUN_TEST(store_search_case_insensitive);
|
||||
|
||||
Reference in new issue
Block a user