mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-03 08:03:38 +08:00
feat(mcp): enable overlay relationship search
Use the active overlay graph read model for search_graph relationship-only requests while keeping connected-neighbor enrichment on canonical ids until that path is separately implemented. Add active_edges CTE support to cbm_store_search_overlay_view so relationship filters, entry-point filters, and degree filters can use visible canonical edges plus latest ready owned overlay edges keyed by qualified name. Make search degree-filter wrapping fail explicitly on allocation or SQL truncation instead of silently truncating generated SQL. Validation: CBM_ONLY_SUITE=store_nodes make -f Makefile.cbm -j8 test (/private/tmp/cbm-pan83-overlay-relationship-store-20260703T2043.log, 93 passed); CBM_ONLY_SUITE=mcp build/c/test-runner (/private/tmp/cbm-pan83-overlay-relationship-mcp-20260703T2048.log, 146 passed); make -f Makefile.cbm -j8 cbm (/private/tmp/cbm-pan83-overlay-relationship-product-build-20260703T2048.log); bash scripts/check-source-safety.sh (/private/tmp/cbm-pan83-overlay-relationship-source-safety-20260703T2048.log); git diff --check (/private/tmp/cbm-pan83-overlay-relationship-diff-check-20260703T2048.log). Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+25
-13
@@ -118,6 +118,7 @@ static void add_response_warning(yyjson_mut_doc *doc, yyjson_mut_val *root, cons
|
||||
#define CBM_MCP_FRESHNESS_STALE_WITH_WARNING "stale_with_warning"
|
||||
#define CBM_MCP_FRESHNESS_READ_MODEL_KEY "read_model"
|
||||
#define CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES "overlay_active_nodes"
|
||||
#define CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_GRAPH "overlay_active_graph"
|
||||
#define CBM_MCP_EXACT_DELTA_KEY "exact_delta"
|
||||
|
||||
static yyjson_mut_val *ensure_response_freshness(yyjson_mut_doc *doc, yyjson_mut_val *root) {
|
||||
@@ -270,7 +271,7 @@ static void add_overlay_node_read_view_summary(yyjson_mut_doc *doc, yyjson_mut_v
|
||||
|
||||
static void add_overlay_active_node_search_freshness(
|
||||
yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
const cbm_store_overlay_node_view_summary_t *summary) {
|
||||
const cbm_store_overlay_node_view_summary_t *summary, bool uses_active_edges) {
|
||||
if (!summary || summary->active_file_tombstones <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -279,7 +280,9 @@ static void add_overlay_active_node_search_freshness(
|
||||
return;
|
||||
}
|
||||
yyjson_mut_obj_add_str(doc, freshness, CBM_MCP_FRESHNESS_READ_MODEL_KEY,
|
||||
CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES);
|
||||
uses_active_edges
|
||||
? CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_GRAPH
|
||||
: CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES);
|
||||
yyjson_mut_obj_add_int(doc, freshness, "overlay_ready_generations",
|
||||
summary->overlay_ready_generations);
|
||||
yyjson_mut_obj_add_int(doc, freshness, "active_file_tombstones",
|
||||
@@ -290,10 +293,15 @@ static void add_overlay_active_node_search_freshness(
|
||||
summary->overlay_owned_nodes_visible);
|
||||
yyjson_mut_obj_add_int(doc, freshness, "total_nodes_visible",
|
||||
summary->total_nodes_visible);
|
||||
add_response_warning(doc, root,
|
||||
"search_graph graph mode used overlay active node rows; edge traversal, "
|
||||
"FTS query mode, and trace results remain canonical until separately "
|
||||
"enabled.");
|
||||
add_response_warning(
|
||||
doc, root,
|
||||
uses_active_edges
|
||||
? "search_graph graph mode used overlay active node and relationship rows; "
|
||||
"connected-neighbor enrichment, FTS query mode, and trace results remain "
|
||||
"canonical until separately enabled."
|
||||
: "search_graph graph mode used overlay active node rows; connected-neighbor "
|
||||
"enrichment, FTS query mode, and trace results remain canonical until "
|
||||
"separately enabled.");
|
||||
}
|
||||
|
||||
static void add_pipeline_exact_delta_stats(yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
@@ -3729,9 +3737,12 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
cbm_store_get_overlay_node_view_summary(store, project, &overlay_summary) ==
|
||||
CBM_STORE_OK &&
|
||||
overlay_summary.active_file_tombstones > 0;
|
||||
bool overlay_edge_semantics_requested =
|
||||
relationship || include_connected || exclude_entry_points;
|
||||
bool overlay_search_used = overlay_ready_for_nodes && !overlay_edge_semantics_requested;
|
||||
bool overlay_active_edges_requested =
|
||||
relationship || exclude_entry_points || min_degree >= 0 || max_degree >= 0 ||
|
||||
(sort_by && (strcmp(sort_by, "degree") == 0 || strcmp(sort_by, "calls") == 0 ||
|
||||
strcmp(sort_by, "linkrank") == 0));
|
||||
bool overlay_requires_canonical_edge_ids = include_connected;
|
||||
bool overlay_search_used = overlay_ready_for_nodes && !overlay_requires_canonical_edge_ids;
|
||||
if (overlay_search_used) {
|
||||
cbm_store_search_overlay_view(store, ¶ms, &out);
|
||||
} else {
|
||||
@@ -3761,12 +3772,13 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
out.node_degree_stale);
|
||||
add_dirty_file_freshness(doc, root, store, project);
|
||||
if (overlay_search_used) {
|
||||
add_overlay_active_node_search_freshness(doc, root, &overlay_summary);
|
||||
} else if (overlay_ready_for_nodes && overlay_edge_semantics_requested) {
|
||||
add_overlay_active_node_search_freshness(doc, root, &overlay_summary,
|
||||
overlay_active_edges_requested);
|
||||
} else if (overlay_ready_for_nodes && overlay_requires_canonical_edge_ids) {
|
||||
add_response_warning(doc, root,
|
||||
"overlay rows are ready, but this search_graph request used "
|
||||
"canonical graph rows because relationship/connected traversal "
|
||||
"needs canonical edge ids until overlay edge reads are enabled.");
|
||||
"canonical graph rows because connected-neighbor enrichment "
|
||||
"needs canonical edge ids until overlay connected reads are enabled.");
|
||||
}
|
||||
if (search_graph_uses_route_derived_graph(label, relationship) &&
|
||||
cbm_store_derived_view_is_stale(store, project, CBM_STORE_DERIVED_VIEW_ROUTES)) {
|
||||
|
||||
+171
-52
@@ -6022,24 +6022,41 @@ static char *make_like_hint(const char *literal) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void search_apply_degree_filter(char *sql, size_t sql_sz, const cbm_search_params_t *p) {
|
||||
static int search_apply_degree_filter(cbm_store_t *s, char *sql, size_t sql_sz,
|
||||
const cbm_search_params_t *p) {
|
||||
bool has_degree_filter = (p->min_degree >= 0 || p->max_degree >= 0);
|
||||
if (!has_degree_filter) {
|
||||
return;
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
char *inner_sql = malloc(sql_sz);
|
||||
if (!inner_sql) {
|
||||
store_set_error(s, "search degree filter out of memory");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
int n = snprintf(inner_sql, sql_sz, "%s", sql);
|
||||
if (n < 0 || (size_t)n >= sql_sz) {
|
||||
free(inner_sql);
|
||||
store_set_error(s, "search degree filter input SQL truncated");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
char inner_sql[CBM_SZ_4K];
|
||||
snprintf(inner_sql, sizeof(inner_sql), "%s", sql);
|
||||
if (p->min_degree >= 0 && p->max_degree >= 0) {
|
||||
snprintf(sql, sql_sz,
|
||||
"SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d AND (in_deg + out_deg) <= %d",
|
||||
inner_sql, p->min_degree, p->max_degree);
|
||||
n = snprintf(sql, sql_sz,
|
||||
"SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d "
|
||||
"AND (in_deg + out_deg) <= %d",
|
||||
inner_sql, p->min_degree, p->max_degree);
|
||||
} else if (p->min_degree >= 0) {
|
||||
snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d", inner_sql,
|
||||
p->min_degree);
|
||||
n = snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d",
|
||||
inner_sql, p->min_degree);
|
||||
} else {
|
||||
snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d", inner_sql,
|
||||
p->max_degree);
|
||||
n = snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d",
|
||||
inner_sql, p->max_degree);
|
||||
}
|
||||
free(inner_sql);
|
||||
if (n < 0 || (size_t)n >= sql_sz) {
|
||||
store_set_error(s, "search degree filter SQL truncated");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
/* Append a WHERE clause fragment, joining with AND if not the first. */
|
||||
@@ -6267,7 +6284,27 @@ static void search_where_advanced(const cbm_search_params_t *params, char *where
|
||||
char excl_clause[CBM_SZ_512];
|
||||
search_build_exclude_labels(params->exclude_labels, binds, bind_idx, excl_clause,
|
||||
(int)sizeof(excl_clause));
|
||||
(void)where_append(where, where_sz, *wlen, nparams, excl_clause);
|
||||
*wlen = where_append(where, where_sz, *wlen, nparams, excl_clause);
|
||||
}
|
||||
}
|
||||
|
||||
static void search_where_overlay_edges(const cbm_search_params_t *params, char *where,
|
||||
int where_sz, int *wlen, int *nparams,
|
||||
search_bind_t *binds, int *bind_idx) {
|
||||
if (params->relationship) {
|
||||
char rel_clause[CBM_SZ_256];
|
||||
snprintf(rel_clause, sizeof(rel_clause),
|
||||
"EXISTS(SELECT 1 FROM active_edges e WHERE "
|
||||
"(e.source_qn = n.qualified_name OR e.target_qn = n.qualified_name) "
|
||||
"AND e.type = ?%d)",
|
||||
*bind_idx + SKIP_ONE);
|
||||
*wlen = where_append(where, where_sz, *wlen, nparams, rel_clause);
|
||||
where_bind_text(binds, bind_idx, params->relationship);
|
||||
}
|
||||
if (params->exclude_entry_points) {
|
||||
*wlen = where_append(where, where_sz, *wlen, nparams,
|
||||
"EXISTS(SELECT 1 FROM active_edges e "
|
||||
"WHERE e.target_qn = n.qualified_name)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6282,6 +6319,79 @@ static int search_build_where(const cbm_search_params_t *params, char *where, in
|
||||
return nparams;
|
||||
}
|
||||
|
||||
static bool search_overlay_needs_active_edges(const cbm_search_params_t *params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
if (params->relationship || params->exclude_entry_points ||
|
||||
params->min_degree >= 0 || params->max_degree >= 0) {
|
||||
return true;
|
||||
}
|
||||
return params->sort_by &&
|
||||
(strcmp(params->sort_by, "degree") == 0 ||
|
||||
strcmp(params->sort_by, "calls") == 0 ||
|
||||
strcmp(params->sort_by, "linkrank") == 0);
|
||||
}
|
||||
|
||||
static int search_build_active_overlay_cte(char *buf, size_t buf_sz, bool include_edges) {
|
||||
int n = snprintf(buf, buf_sz,
|
||||
"WITH active_files AS ("
|
||||
" SELECT t.project, t.rel_path, MAX(t.overlay_generation) AS overlay_generation"
|
||||
" FROM overlay_tombstones t"
|
||||
" JOIN overlay_generations g"
|
||||
" ON g.project = t.project AND g.overlay_generation = t.overlay_generation"
|
||||
" WHERE g.status = ?1 AND t.entity_kind = ?2 AND t.active = %d"
|
||||
" GROUP BY t.project, t.rel_path"
|
||||
"), active_nodes AS ("
|
||||
" SELECT n.id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
|
||||
" n.start_line, n.end_line, n.properties"
|
||||
" FROM nodes n"
|
||||
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
|
||||
" WHERE af.project = n.project AND af.rel_path = n.file_path)"
|
||||
" UNION ALL"
|
||||
" SELECT %d AS id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
|
||||
" n.start_line, n.end_line, n.properties"
|
||||
" FROM overlay_nodes n"
|
||||
" JOIN active_files af"
|
||||
" ON af.project = n.project AND af.rel_path = n.rel_path"
|
||||
" AND af.overlay_generation = n.overlay_generation"
|
||||
" WHERE n.owned = %d"
|
||||
")",
|
||||
STORE_OVERLAY_TOMBSTONE_ACTIVE, CBM_STORE_NO_NODE_ID,
|
||||
STORE_OVERLAY_ROW_OWNED);
|
||||
if (n < 0 || (size_t)n >= buf_sz) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
size_t used = (size_t)n;
|
||||
if (!include_edges) {
|
||||
n = snprintf(buf + used, buf_sz - used, " ");
|
||||
return (n >= 0 && used + (size_t)n < buf_sz) ? CBM_STORE_OK : CBM_STORE_ERR;
|
||||
}
|
||||
n = snprintf(buf + used, buf_sz - used,
|
||||
", active_edges AS ("
|
||||
" SELECT s.qualified_name AS source_qn, t.qualified_name AS target_qn, e.type"
|
||||
" FROM edges e"
|
||||
" JOIN nodes s ON s.id = e.source_id"
|
||||
" JOIN nodes t ON t.id = e.target_id"
|
||||
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
|
||||
" WHERE af.project = s.project AND af.rel_path = s.file_path)"
|
||||
" AND NOT EXISTS (SELECT 1 FROM active_files af"
|
||||
" WHERE af.project = t.project AND af.rel_path = t.file_path)"
|
||||
" UNION ALL"
|
||||
" SELECT e.source_qn, e.target_qn, e.type"
|
||||
" FROM overlay_edges e"
|
||||
" JOIN active_files af"
|
||||
" ON af.project = e.project AND af.rel_path = e.rel_path"
|
||||
" AND af.overlay_generation = e.overlay_generation"
|
||||
" WHERE e.owned = %d"
|
||||
") ",
|
||||
STORE_OVERLAY_ROW_OWNED);
|
||||
if (n < 0 || used + (size_t)n >= buf_sz) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
static int search_execute_sql(cbm_store_t *s, const char *sql, const char *count_sql,
|
||||
search_bind_t *binds, int bind_idx,
|
||||
search_like_pool_t *like_pool, bool use_pagerank,
|
||||
@@ -6483,7 +6593,10 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
|
||||
/* Degree filters — upstream helper wraps in subquery on in_deg/out_deg. */
|
||||
bool has_degree_filter = (params->min_degree >= 0 || params->max_degree >= 0);
|
||||
search_apply_degree_filter(sql, sizeof(sql), params);
|
||||
if (search_apply_degree_filter(s, sql, sizeof(sql), params) != CBM_STORE_OK) {
|
||||
like_pool_free(&like_pool);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
/* Count query — stripped of per-row edge subqueries for the common (no-degree-filter)
|
||||
* case, since we only need the row count, not in_deg/out_deg. The degree-filter
|
||||
@@ -6604,46 +6717,39 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
|
||||
|
||||
bool use_pagerank =
|
||||
(!params->sort_by || strcmp(params->sort_by, "relevance") == 0) && !out->pagerank_stale;
|
||||
bool use_active_edges = search_overlay_needs_active_edges(params);
|
||||
const char *in_degree_expr =
|
||||
use_active_edges
|
||||
? "(SELECT COUNT(*) FROM active_edges e WHERE e.target_qn = n.qualified_name)"
|
||||
: "(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id)";
|
||||
const char *out_degree_expr =
|
||||
use_active_edges
|
||||
? "(SELECT COUNT(*) FROM active_edges e WHERE e.source_qn = n.qualified_name)"
|
||||
: "(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id)";
|
||||
char select_with_pr[CBM_SZ_512];
|
||||
char select_without_pr[CBM_SZ_512];
|
||||
snprintf(select_with_pr, sizeof(select_with_pr),
|
||||
"SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
|
||||
"n.file_path, n.start_line, n.end_line, n.properties, "
|
||||
"%s AS in_deg, %s AS out_deg, COALESCE(pr.rank, 0.0) AS pr_rank ",
|
||||
in_degree_expr, out_degree_expr);
|
||||
snprintf(select_without_pr, sizeof(select_without_pr),
|
||||
"SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
|
||||
"n.file_path, n.start_line, n.end_line, n.properties, "
|
||||
"%s AS in_deg, %s AS out_deg ",
|
||||
in_degree_expr, out_degree_expr);
|
||||
const char *select_cols =
|
||||
use_pagerank
|
||||
? "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) AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id) AS out_deg, "
|
||||
"COALESCE(pr.rank, 0.0) AS pr_rank "
|
||||
: "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) AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id) AS out_deg ";
|
||||
use_pagerank ? select_with_pr : select_without_pr;
|
||||
const char *from_join =
|
||||
use_pagerank ? "FROM active_nodes n LEFT JOIN pagerank pr ON pr.node_id = n.id"
|
||||
: "FROM active_nodes n";
|
||||
|
||||
char active_cte[CBM_SZ_4K];
|
||||
snprintf(active_cte, sizeof(active_cte),
|
||||
"WITH active_files AS ("
|
||||
" SELECT t.project, t.rel_path, MAX(t.overlay_generation) AS overlay_generation"
|
||||
" FROM overlay_tombstones t"
|
||||
" JOIN overlay_generations g"
|
||||
" ON g.project = t.project AND g.overlay_generation = t.overlay_generation"
|
||||
" WHERE g.status = ?1 AND t.entity_kind = ?2 AND t.active = %d"
|
||||
" GROUP BY t.project, t.rel_path"
|
||||
"), active_nodes AS ("
|
||||
" SELECT n.id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
|
||||
" n.start_line, n.end_line, n.properties"
|
||||
" FROM nodes n"
|
||||
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
|
||||
" WHERE af.project = n.project AND af.rel_path = n.file_path)"
|
||||
" UNION ALL"
|
||||
" SELECT %d AS id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
|
||||
" n.start_line, n.end_line, n.properties"
|
||||
" FROM overlay_nodes n"
|
||||
" JOIN active_files af"
|
||||
" ON af.project = n.project AND af.rel_path = n.rel_path"
|
||||
" AND af.overlay_generation = n.overlay_generation"
|
||||
" WHERE n.owned = %d"
|
||||
") ",
|
||||
STORE_OVERLAY_TOMBSTONE_ACTIVE, CBM_STORE_NO_NODE_ID, STORE_OVERLAY_ROW_OWNED);
|
||||
char active_cte[ST_SQL_BUF];
|
||||
if (search_build_active_overlay_cte(active_cte, sizeof(active_cte), use_active_edges) !=
|
||||
CBM_STORE_OK) {
|
||||
store_set_error(s, "search_overlay_view active CTE SQL truncated");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
char where[CBM_SZ_2K] = "";
|
||||
search_bind_t binds[ST_SEARCH_MAX_BINDS];
|
||||
@@ -6651,11 +6757,21 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
|
||||
int bind_idx = 0;
|
||||
where_bind_text(binds, &bind_idx, CBM_STORE_OVERLAY_STATUS_READY);
|
||||
where_bind_text(binds, &bind_idx, CBM_STORE_OVERLAY_TOMBSTONE_FILE);
|
||||
cbm_search_params_t active_params = *params;
|
||||
if (use_active_edges) {
|
||||
active_params.relationship = NULL;
|
||||
active_params.exclude_entry_points = false;
|
||||
}
|
||||
int nparams =
|
||||
search_build_where(params, where, (int)sizeof(where), binds, &bind_idx, &like_pool);
|
||||
search_build_where(&active_params, where, (int)sizeof(where), binds, &bind_idx, &like_pool);
|
||||
int wlen = (int)strlen(where);
|
||||
if (use_active_edges) {
|
||||
search_where_overlay_edges(params, where, (int)sizeof(where), &wlen, &nparams, binds,
|
||||
&bind_idx);
|
||||
}
|
||||
|
||||
char sql[CBM_SZ_4K];
|
||||
char count_sql[CBM_SZ_4K];
|
||||
char sql[ST_SQL_BUF];
|
||||
char count_sql[ST_SQL_BUF];
|
||||
if (nparams > 0) {
|
||||
snprintf(sql, sizeof(sql), "%s%s %s WHERE %s", active_cte, select_cols, from_join, where);
|
||||
snprintf(count_sql, sizeof(count_sql), "%sSELECT COUNT(*) FROM active_nodes n WHERE %s",
|
||||
@@ -6667,7 +6783,10 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
|
||||
}
|
||||
|
||||
bool has_degree_filter = (params->min_degree >= 0 || params->max_degree >= 0);
|
||||
search_apply_degree_filter(sql, sizeof(sql), params);
|
||||
if (search_apply_degree_filter(s, sql, sizeof(sql), params) != CBM_STORE_OK) {
|
||||
like_pool_free(&like_pool);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
if (has_degree_filter) {
|
||||
snprintf(count_sql, sizeof(count_sql), "SELECT COUNT(*) FROM (%s)", sql);
|
||||
}
|
||||
|
||||
@@ -903,6 +903,83 @@ TEST(tool_search_graph_uses_overlay_active_node_rows) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_search_graph_uses_overlay_active_relationship_rows) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(st);
|
||||
const char *proj = "search-overlay-relationship";
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/search-overlay-relationship"),
|
||||
CBM_STORE_OK);
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
|
||||
cbm_node_t old_main = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "old_main",
|
||||
.qualified_name = "search.relationship.old_main",
|
||||
.file_path = "main.c",
|
||||
.properties_json = "{}"};
|
||||
cbm_node_t stable = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "stable",
|
||||
.qualified_name = "search.relationship.stable",
|
||||
.file_path = "stable.c",
|
||||
.properties_json = "{}"};
|
||||
int64_t old_main_id = cbm_store_upsert_node(st, &old_main);
|
||||
int64_t stable_id = cbm_store_upsert_node(st, &stable);
|
||||
ASSERT_GT(old_main_id, 0);
|
||||
ASSERT_GT(stable_id, 0);
|
||||
cbm_edge_t old_edge = {.project = proj,
|
||||
.source_id = old_main_id,
|
||||
.target_id = stable_id,
|
||||
.type = "CALLS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(st, &old_edge), 0);
|
||||
|
||||
int64_t overlay_generation = 0;
|
||||
ASSERT_EQ(cbm_store_reserve_overlay_generation(st, proj, 1, &overlay_generation),
|
||||
CBM_STORE_OK);
|
||||
cbm_node_t new_main = {.project = proj,
|
||||
.label = "Function",
|
||||
.name = "new_main",
|
||||
.qualified_name = "search.relationship.new_main",
|
||||
.file_path = "main.c",
|
||||
.properties_json = "{}"};
|
||||
cbm_store_delta_edge_t new_edge = {.source_qn = "search.relationship.new_main",
|
||||
.target_qn = "search.relationship.stable",
|
||||
.type = "CALLS",
|
||||
.properties_json = "{}",
|
||||
.derived_kind = CBM_STORE_DERIVED_KIND_DIRECT};
|
||||
cbm_store_file_delta_t delta = {.project = proj,
|
||||
.rel_path = "main.c",
|
||||
.generation = 1,
|
||||
.nodes = &new_main,
|
||||
.node_count = 1,
|
||||
.edges = &new_edge,
|
||||
.edge_count = 1};
|
||||
ASSERT_EQ(cbm_store_publish_overlay_file_delta(st, &delta, overlay_generation),
|
||||
CBM_STORE_OK);
|
||||
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":148,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\","
|
||||
"\"arguments\":{\"project\":\"search-overlay-relationship\","
|
||||
"\"relationship\":\"CALLS\",\"sort_by\":\"name\","
|
||||
"\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "new_main"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "stable"));
|
||||
ASSERT_NULL(strstr(inner, "old_main"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"read_model\":\"overlay_active_graph\""));
|
||||
ASSERT_NOT_NULL(strstr(inner, "overlay active node and relationship rows"));
|
||||
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static bool mcp_test_upsert_fts_node(cbm_store_t *st, const char *project, const char *label,
|
||||
const char *name, const char *qualified_name,
|
||||
const char *file_path) {
|
||||
@@ -4228,6 +4305,7 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_search_graph_warns_on_stale_route_view);
|
||||
RUN_TEST(tool_search_graph_reports_dirty_metadata_without_hiding_canonical_rows);
|
||||
RUN_TEST(tool_search_graph_uses_overlay_active_node_rows);
|
||||
RUN_TEST(tool_search_graph_uses_overlay_active_relationship_rows);
|
||||
RUN_TEST(tool_search_graph_query_reports_dirty_metadata_without_hiding_results);
|
||||
RUN_TEST(tool_search_graph_query_sees_file_delta_fts_updates);
|
||||
RUN_TEST(tool_search_graph_query_honors_file_pattern_issue552);
|
||||
|
||||
@@ -1701,6 +1701,104 @@ TEST(store_search_overlay_view_matches_full_rebuild_oracle) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_search_overlay_view_uses_active_relationship_edges) {
|
||||
enum { BASE_GENERATION = 1 };
|
||||
cbm_store_t *live = cbm_store_open_memory();
|
||||
cbm_store_t *oracle = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(live);
|
||||
ASSERT_NOT_NULL(oracle);
|
||||
ASSERT_EQ(cbm_store_upsert_project(live, "test", "/tmp/test"), CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_upsert_project(oracle, "test", "/tmp/test"), CBM_STORE_OK);
|
||||
|
||||
cbm_node_t old_main = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "old_main",
|
||||
.qualified_name = "test.old_main",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"};
|
||||
cbm_node_t stable = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "stable",
|
||||
.qualified_name = "test.stable",
|
||||
.file_path = "stable.go",
|
||||
.properties_json = "{}"};
|
||||
int64_t old_main_id = cbm_store_upsert_node(live, &old_main);
|
||||
int64_t stable_id = cbm_store_upsert_node(live, &stable);
|
||||
ASSERT_GT(old_main_id, 0);
|
||||
ASSERT_GT(stable_id, 0);
|
||||
cbm_edge_t old_edge = {.project = "test",
|
||||
.source_id = old_main_id,
|
||||
.target_id = stable_id,
|
||||
.type = "CALLS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(live, &old_edge), 0);
|
||||
ASSERT_GT(cbm_store_upsert_node(oracle, &stable), 0);
|
||||
|
||||
int64_t overlay_generation = 0;
|
||||
ASSERT_EQ(cbm_store_reserve_overlay_generation(live, "test", BASE_GENERATION,
|
||||
&overlay_generation),
|
||||
CBM_STORE_OK);
|
||||
cbm_node_t new_main = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "new_main",
|
||||
.qualified_name = "test.new_main",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"};
|
||||
cbm_store_delta_edge_t new_edge = {.source_qn = "test.new_main",
|
||||
.target_qn = "test.stable",
|
||||
.type = "CALLS",
|
||||
.properties_json = "{}",
|
||||
.derived_kind = CBM_STORE_DERIVED_KIND_DIRECT};
|
||||
cbm_store_file_delta_t delta = {.project = "test",
|
||||
.rel_path = "main.go",
|
||||
.generation = BASE_GENERATION,
|
||||
.nodes = &new_main,
|
||||
.node_count = 1,
|
||||
.edges = &new_edge,
|
||||
.edge_count = 1};
|
||||
ASSERT_EQ(cbm_store_publish_overlay_file_delta(live, &delta, overlay_generation),
|
||||
CBM_STORE_OK);
|
||||
int64_t new_main_id = cbm_store_upsert_node(oracle, &new_main);
|
||||
ASSERT_GT(new_main_id, 0);
|
||||
int64_t oracle_stable_id = 0;
|
||||
cbm_node_t oracle_stable = {0};
|
||||
ASSERT_EQ(cbm_store_find_node_by_qn(oracle, "test", "test.stable", &oracle_stable),
|
||||
CBM_STORE_OK);
|
||||
oracle_stable_id = oracle_stable.id;
|
||||
cbm_node_free_fields(&oracle_stable);
|
||||
cbm_edge_t oracle_edge = {.project = "test",
|
||||
.source_id = new_main_id,
|
||||
.target_id = oracle_stable_id,
|
||||
.type = "CALLS"};
|
||||
ASSERT_GT(cbm_store_insert_edge(oracle, &oracle_edge), 0);
|
||||
|
||||
cbm_search_params_t params = {.project = "test",
|
||||
.relationship = "CALLS",
|
||||
.sort_by = "name",
|
||||
.limit = 10,
|
||||
.min_degree = 0,
|
||||
.max_degree = -1};
|
||||
cbm_search_output_t active = {0};
|
||||
cbm_search_output_t expected = {0};
|
||||
ASSERT_EQ(cbm_store_search_overlay_view(live, ¶ms, &active), CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_search(oracle, ¶ms, &expected), CBM_STORE_OK);
|
||||
ASSERT_EQ(active.total, expected.total);
|
||||
ASSERT_EQ(active.count, expected.count);
|
||||
ASSERT_EQ(active.count, 2);
|
||||
ASSERT_STR_EQ(active.results[0].node.name, expected.results[0].node.name);
|
||||
ASSERT_STR_EQ(active.results[1].node.name, expected.results[1].node.name);
|
||||
ASSERT_STR_EQ(active.results[0].node.name, "new_main");
|
||||
ASSERT_STR_EQ(active.results[1].node.name, "stable");
|
||||
ASSERT_EQ(active.results[0].node.id, CBM_STORE_NO_NODE_ID);
|
||||
ASSERT_GT(active.results[1].in_degree, 0);
|
||||
ASSERT_GT(active.results[0].out_degree, 0);
|
||||
|
||||
cbm_store_search_free(&active);
|
||||
cbm_store_search_free(&expected);
|
||||
cbm_store_close(live);
|
||||
cbm_store_close(oracle);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_owner_metadata_crud) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
@@ -4440,6 +4538,7 @@ SUITE(store_nodes) {
|
||||
RUN_TEST(store_find_nodes_by_file_overlay_view_returns_latest_ready_rows);
|
||||
RUN_TEST(store_search_overlay_view_without_ready_overlay_matches_canonical_search);
|
||||
RUN_TEST(store_search_overlay_view_matches_full_rebuild_oracle);
|
||||
RUN_TEST(store_search_overlay_view_uses_active_relationship_edges);
|
||||
RUN_TEST(store_owner_metadata_crud);
|
||||
RUN_TEST(store_rebuild_file_delta_owners_derives_from_graph);
|
||||
RUN_TEST(store_import_export_metadata_crud);
|
||||
|
||||
Reference in New Issue
Block a user