feat(store): gate stale derived rankings

Add derived-view freshness flags to store search and traversal results, and use the existing derived_view_state store APIs to suppress explicitly stale PageRank, LinkRank, and node-degree acceleration. Missing freshness rows remain legacy-compatible so old DBs keep their existing rank output.

Mark PageRank, LinkRank, and node-degree views complete after successful rank-table writes using the named unknown-generation constant when no index generation is available. MCP search_graph and trace_path now return structured warnings when stale derived rankings are ignored, without writing protocol-breaking stdout/stderr.

Validation: git diff --check; bash scripts/check-source-safety.sh; make -j8 -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=store_search ./build/c/test-runner; CBM_ONLY_SUITE=pagerank ./build/c/test-runner; CBM_ONLY_SUITE=mcp ./build/c/test-runner; CBM_ONLY_SUITE=store_nodes ./build/c/test-runner.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-06-30 21:24:07 -04:00
parent df7f813bde
commit dc761ef992
8 changed files with 274 additions and 19 deletions
+33
View File
@@ -93,6 +93,30 @@ static void add_pagerank_val(yyjson_mut_doc *doc, yyjson_mut_val *obj, double v)
yyjson_mut_obj_add_val(doc, obj, "pagerank", yyjson_mut_rawcpy(doc, buf));
}
static void add_derived_freshness_warnings(yyjson_mut_doc *doc, yyjson_mut_val *root,
bool pagerank_stale, bool linkrank_stale,
bool node_degree_stale) {
yyjson_mut_val *warnings = yyjson_mut_arr(doc);
if (pagerank_stale) {
yyjson_mut_arr_add_str(
doc, warnings,
"pagerank derived view is stale; stale PageRank values were omitted.");
}
if (linkrank_stale) {
yyjson_mut_arr_add_str(
doc, warnings,
"linkrank derived view is stale; stale LinkRank ordering was not used.");
}
if (node_degree_stale) {
yyjson_mut_arr_add_str(
doc, warnings,
"node_degree derived view is stale; precomputed degree data was not used.");
}
if (yyjson_mut_arr_size(warnings) > 0) {
yyjson_mut_obj_add_val(doc, root, "warnings", warnings);
}
}
/* Default snippet fallback line count (when end_line unknown) */
#define SNIPPET_DEFAULT_LINES 50
@@ -3235,6 +3259,8 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
/* Auto-context: first response gets full architecture/schema/_context header.
* Subsequent responses just get session_project. */
inject_context_once(doc, root, srv, store);
add_derived_freshness_warnings(doc, root, out.pagerank_stale, out.linkrank_stale,
out.node_degree_stale);
if (is_summary) {
/* Summary mode: aggregate counts by label and file (top 20) */
@@ -4542,6 +4568,13 @@ static char *handle_trace_path(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_obj_add_int(doc, root, "callers_total", tr_in.visited_count);
}
add_derived_freshness_warnings(doc, root,
(do_outbound && tr_out.pagerank_stale) ||
(do_inbound && tr_in.pagerank_stale),
(do_outbound && tr_out.linkrank_stale) ||
(do_inbound && tr_in.linkrank_stale),
false);
if (srv->session_project[0])
yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project);
+31 -3
View File
@@ -198,6 +198,9 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
double *w_in = NULL;
id_map_t map = {0};
int N = 0, E = 0, result = -1;
bool pagerank_written = false;
bool linkrank_written = false;
bool node_degree_written = false;
char **node_labels = NULL; /* label per node, parallel to node_ids */
char **node_projects = NULL; /* owning project per node, parallel to node_ids */
@@ -413,12 +416,15 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
"VALUES (?1, ?2, ?3, ?4)";
sqlite3_stmt *ins_stmt = NULL;
if (sqlite3_prepare_v2(db, ins_sql, -1, &ins_stmt, NULL) == SQLITE_OK) {
pagerank_written = true;
for (int i = 0; i < N; i++) {
sqlite3_bind_int64(ins_stmt, 1, node_ids[i]);
sqlite3_bind_text(ins_stmt, 2, node_projects[i], -1, SQLITE_TRANSIENT);
sqlite3_bind_double(ins_stmt, 3, rank[i]);
sqlite3_bind_text(ins_stmt, 4, ts, -1, SQLITE_TRANSIENT);
sqlite3_step(ins_stmt);
if (sqlite3_step(ins_stmt) != SQLITE_DONE) {
pagerank_written = false;
}
sqlite3_reset(ins_stmt);
}
sqlite3_finalize(ins_stmt);
@@ -441,6 +447,7 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
"VALUES (?1, ?2, ?3, ?4)";
sqlite3_stmt *lr_stmt = NULL;
if (sqlite3_prepare_v2(db, lr_sql, -1, &lr_stmt, NULL) == SQLITE_OK) {
linkrank_written = true;
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
for (int e = 0; e < E; e++) {
int s_idx = edges[e].src_idx;
@@ -451,7 +458,9 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
sqlite3_bind_text(lr_stmt, 2, edges[e].project, -1, SQLITE_TRANSIENT);
sqlite3_bind_double(lr_stmt, 3, lr);
sqlite3_bind_text(lr_stmt, 4, ts, -1, SQLITE_TRANSIENT);
sqlite3_step(lr_stmt);
if (sqlite3_step(lr_stmt) != SQLITE_DONE) {
linkrank_written = false;
}
sqlite3_reset(lr_stmt);
}
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
@@ -489,6 +498,7 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)";
sqlite3_stmt *deg_stmt = NULL;
if (sqlite3_prepare_v2(db, deg_sql, -1, &deg_stmt, NULL) == SQLITE_OK) {
node_degree_written = true;
for (int i = 0; i < N; i++) {
sqlite3_bind_int64(deg_stmt, 1, node_ids[i]);
sqlite3_bind_text(deg_stmt, 2, node_projects[i], -1, SQLITE_TRANSIENT);
@@ -500,7 +510,9 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
sqlite3_bind_double(deg_stmt, 8, out_weight[i]);
sqlite3_bind_double(deg_stmt, 9, lr_in ? lr_in[i] : 0.0);
sqlite3_bind_text(deg_stmt, 10, ts, -1, SQLITE_TRANSIENT);
sqlite3_step(deg_stmt);
if (sqlite3_step(deg_stmt) != SQLITE_DONE) {
node_degree_written = false;
}
sqlite3_reset(deg_stmt);
}
sqlite3_finalize(deg_stmt);
@@ -517,6 +529,22 @@ int cbm_pagerank_compute(cbm_store_t *store, const char *project,
cbm_log_info("pagerank.done", "project", project,
"nodes", n_s, "edges", e_s, "iterations", iter_s);
if (pagerank_written) {
(void)cbm_store_set_derived_view_state(store, project, CBM_STORE_DERIVED_VIEW_PAGERANK,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_COMPLETE);
}
if (linkrank_written) {
(void)cbm_store_set_derived_view_state(store, project, CBM_STORE_DERIVED_VIEW_LINKRANK,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_COMPLETE);
}
if (node_degree_written) {
(void)cbm_store_set_derived_view_state(store, project, CBM_STORE_DERIVED_VIEW_NODE_DEGREE,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_COMPLETE);
}
result = N;
cleanup:
+61 -16
View File
@@ -2528,7 +2528,8 @@ int cbm_store_set_derived_view_state(cbm_store_t *s, const char *project,
const char *view_name, int64_t generation,
const char *status) {
if (!s || !s->db || !project || !project[0] || !view_name || !view_name[0] ||
generation <= 0 || !store_derived_status_valid(status)) {
generation < CBM_STORE_DERIVED_GENERATION_UNKNOWN ||
!store_derived_status_valid(status)) {
if (s) {
store_set_error(s, "set_derived_view_state: invalid argument");
}
@@ -2555,7 +2556,8 @@ int cbm_store_set_derived_view_state(cbm_store_t *s, const char *project,
int cbm_store_mark_derived_views_stale(cbm_store_t *s, const char *project,
int64_t generation, const char *const *view_names,
int view_count) {
if (!s || !s->db || !project || !project[0] || generation <= 0 || view_count < 0 ||
if (!s || !s->db || !project || !project[0] ||
generation < CBM_STORE_DERIVED_GENERATION_UNKNOWN || view_count < 0 ||
(view_count > 0 && !view_names)) {
if (s) {
store_set_error(s, "mark_derived_views_stale: invalid argument");
@@ -2631,6 +2633,18 @@ int cbm_store_get_derived_view_state(cbm_store_t *s, const char *project,
return CBM_STORE_OK;
}
bool cbm_store_derived_view_is_stale(cbm_store_t *s, const char *project,
const char *view_name) {
cbm_derived_view_state_t state = {0};
int rc = cbm_store_get_derived_view_state(s, project, view_name, &state);
if (rc != CBM_STORE_OK) {
return false;
}
bool stale = state.status && strcmp(state.status, CBM_STORE_DERIVED_STATUS_STALE) == 0;
cbm_store_derived_view_state_free_fields(&state);
return stale;
}
int cbm_store_reserve_index_generation(cbm_store_t *s, const char *project,
const char *repo_fingerprint,
const char *config_fingerprint,
@@ -3981,14 +3995,27 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
char count_sql[CBM_SZ_4K];
int bind_idx = 0;
/* Conditionally join pagerank table only when sort_by is relevance.
* Avoids JOIN overhead for name/degree sorts. */
bool use_pagerank = (!params->sort_by ||
strcmp(params->sort_by, "relevance") == 0);
const char *freshness_project =
(params->project && params->project[0] && !params->project_pattern) ? params->project : NULL;
out->pagerank_stale =
freshness_project &&
cbm_store_derived_view_is_stale(s, freshness_project, CBM_STORE_DERIVED_VIEW_PAGERANK);
out->linkrank_stale =
freshness_project &&
cbm_store_derived_view_is_stale(s, freshness_project, CBM_STORE_DERIVED_VIEW_LINKRANK);
out->node_degree_stale =
freshness_project &&
cbm_store_derived_view_is_stale(s, freshness_project, CBM_STORE_DERIVED_VIEW_NODE_DEGREE);
/* Conditionally join pagerank table only when sort_by is relevance and the
* freshness ledger has not explicitly marked PageRank stale. Missing ledger
* rows are treated as legacy-compatible, so older indexes keep their rank data. */
bool use_pagerank =
(!params->sort_by || strcmp(params->sort_by, "relevance") == 0) && !out->pagerank_stale;
/* DF-1: Use precomputed node_degree table when available (O(1) JOIN vs O(|E|) subquery).
* HC-6: Falls back to edge COUNT when node_degree is empty. */
bool has_degree_table = false;
{
if (!out->node_degree_stale) {
sqlite3_stmt *check = NULL;
if (sqlite3_prepare_v2(s->db,
"SELECT 1 FROM node_degree LIMIT 1", -1, &check, NULL) == SQLITE_OK) {
@@ -4137,7 +4164,7 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
dep_last, name_col, id_col, limit, offset);
}
} else if (params->sort_by && strcmp(params->sort_by, "linkrank") == 0) {
if (has_degree_table && !has_degree_filter) {
if (has_degree_table && !out->linkrank_stale && !has_degree_filter) {
snprintf(order_limit, sizeof(order_limit),
" ORDER BY COALESCE(nd.linkrank_in, 0) DESC, %s%s, %s LIMIT %d OFFSET %d",
dep_last, name_col, id_col, limit, offset);
@@ -4247,6 +4274,15 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
return rc;
}
out->root = root;
const char *freshness_project = root.project && root.project[0] ? root.project : NULL;
out->pagerank_stale =
freshness_project &&
cbm_store_derived_view_is_stale(s, freshness_project, CBM_STORE_DERIVED_VIEW_PAGERANK);
out->linkrank_stale =
freshness_project &&
cbm_store_derived_view_is_stale(s, freshness_project, CBM_STORE_DERIVED_VIEW_LINKRANK);
bool use_pagerank = !out->pagerank_stale;
bool use_linkrank = !out->linkrank_stale;
/* MERGE: fork delta — build edge type IN clause with ?N parameterized
* placeholders and cap at 16 (bfs_et_count) so the bind loop and clause
@@ -4286,6 +4322,10 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
next_id = "e.target_id";
}
const char *pagerank_select =
use_pagerank ? "COALESCE(pr.rank, 0.0) AS pr_rank " : "0.0 AS pr_rank ";
const char *pagerank_join = use_pagerank ? "LEFT JOIN pagerank pr ON pr.node_id = n.id " : "";
const char *pagerank_order = use_pagerank ? "bfs.hop, pr_rank DESC" : "bfs.hop, n.name, n.id";
snprintf(sql, sizeof(sql),
"WITH RECURSIVE bfs(node_id, hop) AS ("
" SELECT %lld, 0"
@@ -4297,14 +4337,15 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
")"
"SELECT DISTINCT n.id, n.project, n.label, n.name, n.qualified_name, "
"n.file_path, n.start_line, n.end_line, n.properties, bfs.hop, "
"COALESCE(pr.rank, 0.0) AS pr_rank "
"%s"
"FROM bfs "
"JOIN nodes n ON n.id = bfs.node_id "
"LEFT JOIN pagerank pr ON pr.node_id = n.id "
"%s"
"WHERE bfs.hop > 0 " /* exclude root */
"ORDER BY bfs.hop, pr_rank DESC "
"ORDER BY %s "
"LIMIT %d;",
(long long)start_id, next_id, join_cond, types_clause, max_depth, max_results);
(long long)start_id, next_id, join_cond, types_clause, max_depth, pagerank_select,
pagerank_join, pagerank_order, max_results);
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL);
@@ -4368,17 +4409,21 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
/* Build edge query using the same ?N placeholders for edge types.
* types_clause already contains "?1,?2,..." — safe placeholder string. */
char edge_sql[ST_SQL_BUF];
const char *linkrank_select =
use_linkrank ? "COALESCE(lr.rank, 0.0) AS lr_rank " : "0.0 AS lr_rank ";
const char *linkrank_join = use_linkrank ? "LEFT JOIN linkrank lr ON lr.edge_id = e.id " : "";
const char *linkrank_order = use_linkrank ? "lr_rank DESC" : "n1.name, n2.name, e.type";
snprintf(edge_sql, sizeof(edge_sql),
"SELECT n1.name, n2.name, e.type, "
"COALESCE(lr.rank, 0.0) AS lr_rank "
"%s"
"FROM edges e "
"JOIN nodes n1 ON n1.id = e.source_id "
"JOIN nodes n2 ON n2.id = e.target_id "
"LEFT JOIN linkrank lr ON lr.edge_id = e.id "
"%s"
"WHERE e.source_id IN (%s) AND e.target_id IN (%s) "
"AND e.type IN (%s) "
"ORDER BY lr_rank DESC",
id_set, id_set, types_clause);
"ORDER BY %s",
linkrank_select, linkrank_join, id_set, id_set, types_clause, linkrank_order);
sqlite3_stmt *estmt = NULL;
rc = sqlite3_prepare_v2(s->db, edge_sql, CBM_NOT_FOUND, &estmt, NULL);
+8
View File
@@ -31,6 +31,7 @@ typedef struct cbm_store cbm_store_t;
#define CBM_STORE_INDEX_STATUS_FAILED "failed"
#define CBM_STORE_DERIVED_STATUS_STALE "stale"
#define CBM_STORE_DERIVED_STATUS_COMPLETE "complete"
#define CBM_STORE_DERIVED_GENERATION_UNKNOWN 0
#define CBM_STORE_DERIVED_KIND_DIRECT "direct"
#define CBM_STORE_DERIVED_VIEW_NODES_FTS "nodes_fts"
#define CBM_STORE_DERIVED_VIEW_PAGERANK "pagerank"
@@ -221,6 +222,9 @@ typedef struct {
cbm_search_result_t *results;
int count;
int total; /* total before pagination */
bool pagerank_stale;
bool linkrank_stale;
bool node_degree_stale;
} cbm_search_output_t;
/* ── Traversal ──────────────────────────────────────────────────── */
@@ -244,6 +248,8 @@ typedef struct {
int visited_count;
cbm_edge_info_t *edges;
int edge_count;
bool pagerank_stale;
bool linkrank_stale;
} cbm_traverse_result_t;
/* ── Schema introspection ───────────────────────────────────────── */
@@ -580,6 +586,8 @@ int cbm_store_mark_derived_views_stale(cbm_store_t *s, const char *project,
int cbm_store_get_derived_view_state(cbm_store_t *s, const char *project,
const char *view_name,
cbm_derived_view_state_t *out);
bool cbm_store_derived_view_is_stale(cbm_store_t *s, const char *project,
const char *view_name);
int cbm_store_publish_file_delta(cbm_store_t *s, const cbm_store_file_delta_t *delta);
int cbm_store_publish_file_delta_batch(cbm_store_t *s,
+44
View File
@@ -696,6 +696,49 @@ TEST(tool_search_graph_includes_node_properties) {
PASS();
}
TEST(tool_search_graph_warns_on_stale_pagerank_view) {
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);
ASSERT_EQ(cbm_store_upsert_project(st, "test", "/tmp/test"), CBM_STORE_OK);
cbm_mcp_server_set_project(srv, "test");
cbm_node_t node = {.project = "test",
.label = "Function",
.name = "Handle",
.qualified_name = "test.Handle",
.file_path = "handle.c"};
int64_t id = cbm_store_upsert_node(st, &node);
ASSERT_TRUE(id > 0);
char rank_sql[256];
snprintf(rank_sql, sizeof(rank_sql),
"INSERT INTO pagerank(project,node_id,rank,computed_at) "
"VALUES('test',%lld,0.9,'2026-06-30T00:00:00Z')",
(long long)id);
ASSERT_EQ(cbm_store_exec(st, rank_sql), CBM_STORE_OK);
ASSERT_EQ(cbm_store_set_derived_view_state(st, "test", CBM_STORE_DERIVED_VIEW_PAGERANK,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_STALE),
CBM_STORE_OK);
char *resp = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":43,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_graph\","
"\"arguments\":{\"project\":\"test\",\"label\":\"Function\",\"limit\":5}}}");
ASSERT_NOT_NULL(resp);
char *inner = extract_text_content(resp);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"warnings\""));
ASSERT_NOT_NULL(strstr(inner, "pagerank derived view is stale"));
ASSERT_NULL(strstr(inner, "\"pagerank\":"));
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) {
@@ -3036,6 +3079,7 @@ SUITE(mcp) {
RUN_TEST(tool_unknown_tool);
RUN_TEST(tool_search_graph_basic);
RUN_TEST(tool_search_graph_includes_node_properties);
RUN_TEST(tool_search_graph_warns_on_stale_pagerank_view);
RUN_TEST(tool_search_graph_query_honors_file_pattern_issue552);
RUN_TEST(tool_search_graph_query_uses_search_limit_config);
RUN_TEST(tool_search_graph_query_rejects_bad_semantic_query);
+21
View File
@@ -213,6 +213,27 @@ TEST(pagerank_stored_in_db) {
add_node(s, "db", "f2");
cbm_pagerank_compute_default(s, "db");
ASSERT_EQ(count_table_rows(s, "pagerank"), 2);
cbm_derived_view_state_t state = {0};
ASSERT_EQ(cbm_store_get_derived_view_state(s, "db", CBM_STORE_DERIVED_VIEW_PAGERANK,
&state),
CBM_STORE_OK);
ASSERT_STR_EQ(state.status, CBM_STORE_DERIVED_STATUS_COMPLETE);
ASSERT_EQ(state.source_generation, CBM_STORE_DERIVED_GENERATION_UNKNOWN);
cbm_store_derived_view_state_free_fields(&state);
ASSERT_EQ(cbm_store_get_derived_view_state(s, "db", CBM_STORE_DERIVED_VIEW_LINKRANK,
&state),
CBM_STORE_OK);
ASSERT_STR_EQ(state.status, CBM_STORE_DERIVED_STATUS_COMPLETE);
cbm_store_derived_view_state_free_fields(&state);
ASSERT_EQ(cbm_store_get_derived_view_state(s, "db", CBM_STORE_DERIVED_VIEW_NODE_DEGREE,
&state),
CBM_STORE_OK);
ASSERT_STR_EQ(state.status, CBM_STORE_DERIVED_STATUS_COMPLETE);
cbm_store_derived_view_state_free_fields(&state);
cbm_store_close(s);
PASS();
}
+15
View File
@@ -1936,6 +1936,8 @@ TEST(store_derived_view_state_public_api) {
ASSERT_EQ(cbm_store_get_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_NODE_DEGREE,
&got),
CBM_STORE_OK);
ASSERT_TRUE(cbm_store_derived_view_is_stale(s, "test",
CBM_STORE_DERIVED_VIEW_NODE_DEGREE));
ASSERT_STR_EQ(got.project, "test");
ASSERT_STR_EQ(got.view_name, CBM_STORE_DERIVED_VIEW_NODE_DEGREE);
ASSERT_EQ(got.source_generation, STALE_GENERATION);
@@ -1960,6 +1962,19 @@ TEST(store_derived_view_state_public_api) {
ASSERT_EQ(got.source_generation, COMPLETE_GENERATION);
ASSERT_STR_EQ(got.status, CBM_STORE_DERIVED_STATUS_COMPLETE);
cbm_store_derived_view_state_free_fields(&got);
ASSERT_FALSE(cbm_store_derived_view_is_stale(s, "test",
CBM_STORE_DERIVED_VIEW_PAGERANK));
ASSERT_EQ(cbm_store_set_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_ARCHITECTURE,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_COMPLETE),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_get_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_ARCHITECTURE,
&got),
CBM_STORE_OK);
ASSERT_EQ(got.source_generation, CBM_STORE_DERIVED_GENERATION_UNKNOWN);
ASSERT_STR_EQ(got.status, CBM_STORE_DERIVED_STATUS_COMPLETE);
cbm_store_derived_view_state_free_fields(&got);
ASSERT_EQ(cbm_store_set_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_LINKRANK,
COMPLETE_GENERATION,
+61
View File
@@ -1020,6 +1020,66 @@ TEST(store_bfs_carries_joined_pagerank_score) {
ASSERT_FLOAT_EQ(result.visited[0].pagerank_score, 0.75, CBM_PAGERANK_EPSILON);
cbm_store_traverse_free(&result);
ASSERT_EQ(cbm_store_set_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_PAGERANK,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_STALE),
CBM_STORE_OK);
cbm_traverse_result_t stale_result = {0};
ASSERT_EQ(cbm_store_bfs(s, idA, "outbound", types, 1, 1, 10, &stale_result),
CBM_STORE_OK);
ASSERT_TRUE(stale_result.pagerank_stale);
ASSERT_EQ(stale_result.visited_count, 1);
ASSERT_FLOAT_EQ(stale_result.visited[0].pagerank_score, 0.0, CBM_PAGERANK_EPSILON);
cbm_store_traverse_free(&stale_result);
cbm_store_close(s);
PASS();
}
TEST(store_search_uses_legacy_but_not_stale_pagerank) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t na = {
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
cbm_node_t nb = {
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
int64_t idA = cbm_store_upsert_node(s, &na);
int64_t idB = cbm_store_upsert_node(s, &nb);
char rank_sql[512];
snprintf(rank_sql, sizeof(rank_sql),
"INSERT INTO pagerank(project,node_id,rank,computed_at) "
"VALUES('test',%lld,0.1,'2026-06-30T00:00:00Z'),"
"('test',%lld,0.9,'2026-06-30T00:00:00Z')",
(long long)idA, (long long)idB);
ASSERT_EQ(cbm_store_exec(s, rank_sql), CBM_STORE_OK);
cbm_search_params_t params = {0};
params.project = "test";
params.limit = 2;
params.min_degree = -1;
params.max_degree = -1;
cbm_search_output_t out = {0};
ASSERT_EQ(cbm_store_search(s, &params, &out), CBM_STORE_OK);
ASSERT_FALSE(out.pagerank_stale);
ASSERT_EQ(out.count, 2);
ASSERT_STR_EQ(out.results[0].node.name, "B");
ASSERT_FLOAT_EQ(out.results[0].pagerank_score, 0.9, CBM_PAGERANK_EPSILON);
cbm_store_search_free(&out);
ASSERT_EQ(cbm_store_set_derived_view_state(s, "test", CBM_STORE_DERIVED_VIEW_PAGERANK,
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
CBM_STORE_DERIVED_STATUS_STALE),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_search(s, &params, &out), CBM_STORE_OK);
ASSERT_TRUE(out.pagerank_stale);
ASSERT_EQ(out.count, 2);
ASSERT_STR_EQ(out.results[0].node.name, "A");
ASSERT_FLOAT_EQ(out.results[0].pagerank_score, 0.0, CBM_PAGERANK_EPSILON);
cbm_store_search_free(&out);
cbm_store_close(s);
PASS();
}
@@ -1628,6 +1688,7 @@ SUITE(store_search) {
RUN_TEST(store_deduplicate_hops);
RUN_TEST(store_bfs_with_risk_labels);
RUN_TEST(store_bfs_carries_joined_pagerank_score);
RUN_TEST(store_search_uses_legacy_but_not_stale_pagerank);
RUN_TEST(store_bfs_cross_service_summary);
RUN_TEST(store_glob_to_like);
RUN_TEST(store_extract_like_hints);