mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
merge pagerank-ranking: add PageRank node + LinkRank edge ranking
Merge pagerank-ranking branch into combined token-reduction + reference-api-indexing branch. Resolves conflicts in mcp.c (sort_by coexists with compact/summary/token-reduction params), Makefile.cbm (restore TEST_TOKEN_REDUCTION_SRCS lost in merge). All 2119 tests pass (35 PageRank + 22 token-reduction + 2062 existing).
This commit is contained in:
+9
-4
@@ -180,6 +180,9 @@ PIPELINE_SRCS = \
|
||||
# Depindex module (dependency/reference API indexing)
|
||||
DEPINDEX_SRCS = src/depindex/depindex.c
|
||||
|
||||
# PageRank module (node + edge ranking)
|
||||
PAGERANK_SRCS = src/pagerank/pagerank.c
|
||||
|
||||
# Traces module (new)
|
||||
TRACES_SRCS = src/traces/traces.c
|
||||
|
||||
@@ -226,7 +229,7 @@ TRE_CFLAGS = -std=c11 -g -O1 -w -Ivendored/tre
|
||||
YYJSON_SRC = vendored/yyjson/yyjson.c
|
||||
|
||||
# All production sources
|
||||
PROD_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(DEPINDEX_SRCS) $(TRACES_SRCS) $(WATCHER_SRCS) $(CLI_SRCS) $(UI_SRCS) $(YYJSON_SRC)
|
||||
PROD_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(DEPINDEX_SRCS) $(PAGERANK_SRCS) $(TRACES_SRCS) $(WATCHER_SRCS) $(CLI_SRCS) $(UI_SRCS) $(YYJSON_SRC)
|
||||
EXISTING_C_SRCS = $(EXTRACTION_SRCS) $(LSP_SRCS) $(TS_RUNTIME_SRC) \
|
||||
$(GRAMMAR_SRCS) $(AC_LZ4_SRCS) $(SQLITE_WRITER_SRC)
|
||||
|
||||
@@ -289,11 +292,13 @@ TEST_MEM_SRCS = tests/test_mem.c
|
||||
|
||||
TEST_UI_SRCS = tests/test_ui.c
|
||||
|
||||
TEST_TOKEN_REDUCTION_SRCS = tests/test_token_reduction.c
|
||||
|
||||
TEST_DEPINDEX_SRCS = tests/test_depindex.c
|
||||
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_HTTPLINK_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_TOKEN_REDUCTION_SRCS) $(TEST_DEPINDEX_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
TEST_PAGERANK_SRCS = tests/test_pagerank.c
|
||||
|
||||
TEST_TOKEN_REDUCTION_SRCS = tests/test_token_reduction.c
|
||||
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_HTTPLINK_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_DEPINDEX_SRCS) $(TEST_PAGERANK_SRCS) $(TEST_TOKEN_REDUCTION_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
# ── Build directories ────────────────────────────────────────────
|
||||
|
||||
|
||||
+11
-11
@@ -373,18 +373,12 @@ int cbm_dep_auto_index(const char *project_name, const char *project_root,
|
||||
int cbm_dep_link_cross_edges(cbm_store_t *store, const char *project_name) {
|
||||
if (!store || !project_name || !project_name[0]) return 0;
|
||||
|
||||
/* Build dep project LIKE pattern once (invariant across loop) */
|
||||
char dep_pattern[CBM_NAME_MAX];
|
||||
snprintf(dep_pattern, sizeof(dep_pattern), "%s" CBM_DEP_SEPARATOR "%%",
|
||||
project_name);
|
||||
|
||||
/* Find Variable nodes in the project — import statements are extracted as
|
||||
* Variable nodes by tree-sitter extractors (extract_imports.c). */
|
||||
/* Find all IMPORTS nodes in the main project */
|
||||
cbm_search_params_t params = {0};
|
||||
params.project = project_name;
|
||||
params.project_exact = true;
|
||||
params.label = "Variable";
|
||||
params.limit = CBM_DEFAULT_AUTO_DEP_LIMIT;
|
||||
params.label = "Variable"; /* import statements are typically Variable nodes */
|
||||
params.limit = 500;
|
||||
|
||||
cbm_search_output_t out = {0};
|
||||
int rc = cbm_store_search(store, ¶ms, &out);
|
||||
@@ -395,12 +389,17 @@ int cbm_dep_link_cross_edges(cbm_store_t *store, const char *project_name) {
|
||||
|
||||
int linked = 0;
|
||||
|
||||
/* For each import variable, look for a matching Module in dep projects */
|
||||
/* For each import, look for a matching Module in dep projects */
|
||||
for (int i = 0; i < out.count; i++) {
|
||||
const char *import_name = out.results[i].node.name;
|
||||
if (!import_name || !import_name[0]) continue;
|
||||
|
||||
/* Search for Module with matching name across all dep projects */
|
||||
/* Build dep project pattern: project_name.dep.% */
|
||||
char dep_pattern[CBM_NAME_MAX];
|
||||
snprintf(dep_pattern, sizeof(dep_pattern), "%s" CBM_DEP_SEPARATOR "%%",
|
||||
project_name);
|
||||
|
||||
/* Search for Module with matching name in dep projects */
|
||||
cbm_search_params_t dep_params = {0};
|
||||
dep_params.name_pattern = import_name;
|
||||
dep_params.project_pattern = dep_pattern;
|
||||
@@ -410,6 +409,7 @@ int cbm_dep_link_cross_edges(cbm_store_t *store, const char *project_name) {
|
||||
cbm_search_output_t dep_out = {0};
|
||||
int drc = cbm_store_search(store, &dep_params, &dep_out);
|
||||
if (drc == 0 && dep_out.count > 0) {
|
||||
/* Create cross-boundary IMPORTS edge */
|
||||
cbm_edge_t edge = {
|
||||
.source_id = out.results[i].node.id,
|
||||
.target_id = dep_out.results[0].node.id,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "store/store.h"
|
||||
#include "depindex/depindex.h"
|
||||
#include "pagerank/pagerank.h"
|
||||
#include "cli/cli.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/compat_thread.h"
|
||||
@@ -94,6 +95,7 @@ static int watcher_index_fn(const char *project_name, const char *root_path, voi
|
||||
cbm_store_t *store = cbm_store_open(pname);
|
||||
if (store) {
|
||||
cbm_dep_auto_index(pname, root_path, store, CBM_DEFAULT_AUTO_DEP_LIMIT);
|
||||
cbm_pagerank_compute_default(store, pname);
|
||||
cbm_store_close(store);
|
||||
}
|
||||
free(pname);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "cypher/cypher.h"
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "depindex/depindex.h"
|
||||
#include "pagerank/pagerank.h"
|
||||
#include "cli/cli.h"
|
||||
#include "watcher/watcher.h"
|
||||
#include "foundation/mem.h"
|
||||
@@ -274,6 +275,9 @@ static const tool_def_t TOOLS[] = {
|
||||
"Response includes has_more and pagination_hint when more pages exist."
|
||||
"\"},\"offset\":{\"type\":\"integer\",\"default\":0,\"description\":\"Skip N results "
|
||||
"for pagination. Check pagination_hint in response for next page offset.\"},"
|
||||
"\"sort_by\":{\"type\":\"string\",\"enum\":[\"relevance\",\"name\",\"degree\"],"
|
||||
"\"description\":\"Sort order: relevance (PageRank structural importance, default), "
|
||||
"name (alphabetical), degree (most connected).\"},"
|
||||
"\"mode\":{\"type\":\"string\",\"enum\":[\"full\",\"summary\"],\"default\":\"full\","
|
||||
"\"description\":\"full=individual results (default), summary=aggregate counts by label and "
|
||||
"file. Use summary first to understand scope, then full with filters to drill down."
|
||||
@@ -1050,6 +1054,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *label = cbm_mcp_get_string_arg(args, "label");
|
||||
char *name_pattern = cbm_mcp_get_string_arg(args, "name_pattern");
|
||||
char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
|
||||
char *sort_by = cbm_mcp_get_string_arg(args, "sort_by");
|
||||
int cfg_search_limit = cbm_config_get_int(srv->config, CBM_CONFIG_SEARCH_LIMIT,
|
||||
CBM_DEFAULT_SEARCH_LIMIT);
|
||||
int limit = cbm_mcp_get_int_arg(args, "limit", cfg_search_limit);
|
||||
@@ -1068,6 +1073,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
params.label = label;
|
||||
params.name_pattern = name_pattern;
|
||||
params.file_pattern = file_pattern;
|
||||
params.sort_by = sort_by;
|
||||
params.limit = effective_limit;
|
||||
params.offset = offset;
|
||||
params.min_degree = min_degree;
|
||||
@@ -1153,6 +1159,8 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
sr->node.file_path ? sr->node.file_path : "");
|
||||
yyjson_mut_obj_add_int(doc, item, "in_degree", sr->in_degree);
|
||||
yyjson_mut_obj_add_int(doc, item, "out_degree", sr->out_degree);
|
||||
if (sr->pagerank_score > 0.0)
|
||||
yyjson_mut_obj_add_real(doc, item, "pagerank", sr->pagerank_score);
|
||||
|
||||
/* Unconditional source tagging — critical for AI grounding.
|
||||
* Every result tagged source:"project" or source:"dependency".
|
||||
@@ -1190,6 +1198,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(name_pattern);
|
||||
free(file_pattern);
|
||||
free(search_mode);
|
||||
free(sort_by);
|
||||
|
||||
char *result = cbm_mcp_text_result(json, false);
|
||||
free(json);
|
||||
@@ -1568,6 +1577,11 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
doc, item, "qualified_name",
|
||||
tr_out.visited[i].node.qualified_name ? tr_out.visited[i].node.qualified_name : "");
|
||||
yyjson_mut_obj_add_int(doc, item, "hop", tr_out.visited[i].hop);
|
||||
{
|
||||
double pr = cbm_pagerank_get(store, tr_out.visited[i].node.id);
|
||||
if (pr > 0.0)
|
||||
yyjson_mut_obj_add_real(doc, item, "pagerank", pr);
|
||||
}
|
||||
/* Boundary tagging: mark if callee is in a dependency */
|
||||
bool callee_dep = cbm_is_dep_project(tr_out.visited[i].node.project,
|
||||
srv->session_project);
|
||||
@@ -1610,6 +1624,11 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
doc, item, "qualified_name",
|
||||
tr_in.visited[i].node.qualified_name ? tr_in.visited[i].node.qualified_name : "");
|
||||
yyjson_mut_obj_add_int(doc, item, "hop", tr_in.visited[i].hop);
|
||||
{
|
||||
double pr = cbm_pagerank_get(store, tr_in.visited[i].node.id);
|
||||
if (pr > 0.0)
|
||||
yyjson_mut_obj_add_real(doc, item, "pagerank", pr);
|
||||
}
|
||||
/* Boundary tagging: mark if caller is in a dependency */
|
||||
bool caller_dep = cbm_is_dep_project(tr_in.visited[i].node.project,
|
||||
srv->session_project);
|
||||
@@ -1774,6 +1793,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
int deps_reindexed = cbm_dep_auto_index(
|
||||
project_name, repo_path, store, CBM_DEFAULT_AUTO_DEP_LIMIT);
|
||||
|
||||
/* Compute PageRank + LinkRank on full graph (project + deps) */
|
||||
cbm_pagerank_compute_default(store, project_name);
|
||||
|
||||
int nodes = cbm_store_count_nodes(store, project_name);
|
||||
int edges = cbm_store_count_edges(store, project_name);
|
||||
yyjson_mut_obj_add_int(doc, root, "nodes", nodes);
|
||||
@@ -2761,6 +2783,9 @@ static char *handle_index_dependencies(cbm_mcp_server_t *srv, const char *args)
|
||||
if (srv->session_project[0])
|
||||
yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project);
|
||||
|
||||
/* Recompute PageRank after adding dep nodes so relevance sort includes them */
|
||||
cbm_pagerank_compute_default(store, project);
|
||||
|
||||
char *json = yy_doc_to_str(doc);
|
||||
yyjson_mut_doc_free(doc);
|
||||
yyjson_doc_free(doc_args);
|
||||
@@ -2898,6 +2923,7 @@ static void *autoindex_thread(void *arg) {
|
||||
if (store) {
|
||||
cbm_dep_auto_index(srv->session_project, srv->session_root,
|
||||
store, CBM_DEFAULT_AUTO_DEP_LIMIT);
|
||||
cbm_pagerank_compute_default(store, srv->session_project);
|
||||
}
|
||||
|
||||
cbm_log_info("autoindex.done", "project", srv->session_project);
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* pagerank.c — PageRank (node) + LinkRank (edge) ranking for codebase graphs.
|
||||
*
|
||||
* References:
|
||||
* - aider repomap.py (github.com/Aider-AI/aider/blob/main/aider/repomap.py)
|
||||
* - NetworkX pagerank (networkx/algorithms/link_analysis/pagerank_alg.py)
|
||||
* - Kim et al. (2010) LinkRank, arXiv:0902.3728
|
||||
* - nazgob/PageRank (github.com/nazgob/PageRank/blob/master/algorithm.c)
|
||||
*/
|
||||
|
||||
#include "pagerank.h"
|
||||
#include <foundation/log.h>
|
||||
#include <foundation/platform.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
/* ── Default edge weights (aider/RepoMapper-inspired) ──────── */
|
||||
|
||||
const cbm_edge_weights_t CBM_DEFAULT_EDGE_WEIGHTS = {
|
||||
.calls = 1.0, .defines_method = 0.8, .defines = 0.5,
|
||||
.imports = 0.3, .usage = 0.2, .configures = 0.1,
|
||||
.http_calls = 0.5, .async_calls = 0.8, .default_weight = 0.3
|
||||
};
|
||||
|
||||
/* ── Edge weight lookup (ordered by frequency) ─────────────── */
|
||||
|
||||
static double edge_type_weight(const cbm_edge_weights_t *w, const char *type) {
|
||||
if (!type) return w->default_weight;
|
||||
if (strcmp(type, "CALLS") == 0) return w->calls;
|
||||
if (strcmp(type, "IMPORTS") == 0) return w->imports;
|
||||
if (strcmp(type, "USAGE") == 0) return w->usage;
|
||||
if (strcmp(type, "DEFINES") == 0) return w->defines;
|
||||
if (strcmp(type, "DEFINES_METHOD") == 0) return w->defines_method;
|
||||
if (strcmp(type, "CONFIGURES") == 0) return w->configures;
|
||||
if (strcmp(type, "HTTP_CALLS") == 0) return w->http_calls;
|
||||
if (strcmp(type, "ASYNC_CALLS") == 0) return w->async_calls;
|
||||
return w->default_weight;
|
||||
}
|
||||
|
||||
/* ── Internal edge struct ────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
int src_idx;
|
||||
int dst_idx;
|
||||
int64_t edge_id;
|
||||
double weight;
|
||||
} pr_edge_t;
|
||||
|
||||
/* ── ISO timestamp helper ────────────────────────────────────── */
|
||||
|
||||
static void iso_now(char *buf, size_t sz) {
|
||||
time_t t = time(NULL);
|
||||
struct tm tm;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &t);
|
||||
#else
|
||||
gmtime_r(&t, &tm);
|
||||
#endif
|
||||
strftime(buf, sz, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||
}
|
||||
|
||||
/* ── Hash map: node_id -> array index (linear probing) ──────── */
|
||||
|
||||
typedef struct {
|
||||
int64_t *keys;
|
||||
int *vals;
|
||||
int cap;
|
||||
} id_map_t;
|
||||
|
||||
static int id_map_init(id_map_t *m, int n) {
|
||||
m->cap = n * CBM_HASHMAP_LOAD_FACTOR + 1;
|
||||
m->keys = calloc((size_t)m->cap, sizeof(int64_t));
|
||||
m->vals = calloc((size_t)m->cap, sizeof(int));
|
||||
if (!m->keys || !m->vals) {
|
||||
free(m->keys); free(m->vals);
|
||||
m->keys = NULL; m->vals = NULL;
|
||||
return -1;
|
||||
}
|
||||
memset(m->vals, -1, (size_t)m->cap * sizeof(int));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void id_map_put(id_map_t *m, int64_t key, int val) {
|
||||
int h = (int)((uint64_t)key % (uint64_t)m->cap);
|
||||
while (m->keys[h] != 0 && m->keys[h] != key)
|
||||
h = (h + 1) % m->cap;
|
||||
m->keys[h] = key;
|
||||
m->vals[h] = val;
|
||||
}
|
||||
|
||||
static int id_map_get(const id_map_t *m, int64_t key) {
|
||||
int h = (int)((uint64_t)key % (uint64_t)m->cap);
|
||||
while (m->keys[h] != 0) {
|
||||
if (m->keys[h] == key) return m->vals[h];
|
||||
h = (h + 1) % m->cap;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void id_map_free(id_map_t *m) {
|
||||
free(m->keys);
|
||||
free(m->vals);
|
||||
m->keys = NULL;
|
||||
m->vals = NULL;
|
||||
}
|
||||
|
||||
/* ── Scope -> SQL WHERE clause (DRY: one function) ──────────── */
|
||||
|
||||
static const char *scope_where(cbm_rank_scope_t scope) {
|
||||
switch (scope) {
|
||||
case CBM_RANK_SCOPE_PROJECT: return "project = ?1";
|
||||
case CBM_RANK_SCOPE_DEPS: return "project LIKE ?1 || '.dep.%'";
|
||||
case CBM_RANK_SCOPE_FULL:
|
||||
default: return "(project = ?1 OR project LIKE ?1 || '.dep.%')";
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Core PageRank + LinkRank ────────────────────────────────── */
|
||||
|
||||
int cbm_pagerank_compute(cbm_store_t *store, const char *project,
|
||||
double damping, double epsilon, int max_iter,
|
||||
const cbm_edge_weights_t *weights,
|
||||
cbm_rank_scope_t scope) {
|
||||
if (!store || !project || !project[0]) return -1;
|
||||
if (!weights) weights = &CBM_DEFAULT_EDGE_WEIGHTS;
|
||||
if (damping < 0.0 || damping > 1.0) damping = CBM_PAGERANK_DAMPING;
|
||||
if (max_iter <= 0) max_iter = CBM_PAGERANK_MAX_ITER;
|
||||
if (epsilon <= 0.0) epsilon = CBM_PAGERANK_EPSILON;
|
||||
|
||||
sqlite3 *db = cbm_store_get_db(store);
|
||||
if (!db) return -1;
|
||||
|
||||
/* All heap pointers initialized to NULL for safe cleanup via goto */
|
||||
int64_t *node_ids = NULL;
|
||||
pr_edge_t *edges = NULL;
|
||||
double *out_weight = NULL, *rank = NULL, *new_rank = NULL;
|
||||
id_map_t map = {0};
|
||||
int N = 0, E = 0, result = -1;
|
||||
|
||||
/* ── Step 1: Load node IDs ────────────────────────────── */
|
||||
char sql_buf[512];
|
||||
snprintf(sql_buf, sizeof(sql_buf), "SELECT id FROM nodes WHERE %s",
|
||||
scope_where(scope));
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql_buf, -1, &stmt, NULL) != SQLITE_OK)
|
||||
return -1;
|
||||
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT);
|
||||
|
||||
int cap = CBM_PAGERANK_INITIAL_CAP;
|
||||
node_ids = malloc((size_t)cap * sizeof(int64_t));
|
||||
if (!node_ids) { sqlite3_finalize(stmt); return -1; }
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
if (N >= cap) {
|
||||
cap *= 2;
|
||||
node_ids = safe_realloc(node_ids, (size_t)cap * sizeof(int64_t));
|
||||
if (!node_ids) { sqlite3_finalize(stmt); return -1; }
|
||||
}
|
||||
node_ids[N++] = sqlite3_column_int64(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
|
||||
if (N == 0) { free(node_ids); return 0; }
|
||||
|
||||
/* Build id->index map */
|
||||
if (id_map_init(&map, N) != 0) { free(node_ids); return -1; }
|
||||
for (int i = 0; i < N; i++) id_map_put(&map, node_ids[i], i);
|
||||
|
||||
/* ── Step 2: Load weighted edges ──────────────────────── */
|
||||
snprintf(sql_buf, sizeof(sql_buf),
|
||||
"SELECT id, source_id, target_id, type FROM edges WHERE %s",
|
||||
scope_where(scope));
|
||||
if (sqlite3_prepare_v2(db, sql_buf, -1, &stmt, NULL) != SQLITE_OK)
|
||||
goto cleanup;
|
||||
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT);
|
||||
|
||||
int ecap = CBM_PAGERANK_INITIAL_CAP;
|
||||
edges = malloc((size_t)ecap * sizeof(pr_edge_t));
|
||||
if (!edges) { sqlite3_finalize(stmt); goto cleanup; }
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int64_t eid = sqlite3_column_int64(stmt, 0);
|
||||
int64_t src = sqlite3_column_int64(stmt, 1);
|
||||
int64_t dst = sqlite3_column_int64(stmt, 2);
|
||||
const char *type = (const char *)sqlite3_column_text(stmt, 3);
|
||||
|
||||
int si = id_map_get(&map, src);
|
||||
int di = id_map_get(&map, dst);
|
||||
if (si < 0 || di < 0) continue;
|
||||
|
||||
if (E >= ecap) {
|
||||
ecap *= 2;
|
||||
edges = safe_realloc(edges, (size_t)ecap * sizeof(pr_edge_t));
|
||||
if (!edges) { sqlite3_finalize(stmt); goto cleanup; }
|
||||
}
|
||||
edges[E].src_idx = si;
|
||||
edges[E].dst_idx = di;
|
||||
edges[E].edge_id = eid;
|
||||
edges[E].weight = edge_type_weight(weights, type);
|
||||
E++;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
|
||||
/* ── Step 3: Allocate computation buffers ─────────────── */
|
||||
out_weight = calloc((size_t)N, sizeof(double));
|
||||
rank = malloc((size_t)N * sizeof(double));
|
||||
new_rank = malloc((size_t)N * sizeof(double));
|
||||
if (!out_weight || !rank || !new_rank) goto cleanup;
|
||||
|
||||
for (int e = 0; e < E; e++)
|
||||
out_weight[edges[e].src_idx] += edges[e].weight;
|
||||
|
||||
/* ── Step 4: Power iteration ──────────────────────────── */
|
||||
double init_rank = 1.0 / N;
|
||||
for (int i = 0; i < N; i++) rank[i] = init_rank;
|
||||
|
||||
double base = (1.0 - damping) / N;
|
||||
int iter;
|
||||
for (iter = 0; iter < max_iter; iter++) {
|
||||
for (int i = 0; i < N; i++) new_rank[i] = base;
|
||||
|
||||
/* Distribute rank along weighted edges */
|
||||
for (int e = 0; e < E; e++) {
|
||||
int s = edges[e].src_idx;
|
||||
if (out_weight[s] > 0.0) {
|
||||
new_rank[edges[e].dst_idx] +=
|
||||
damping * rank[s] * edges[e].weight / out_weight[s];
|
||||
}
|
||||
}
|
||||
|
||||
/* Dangling node handling (NetworkX convention) */
|
||||
double dangling_sum = 0.0;
|
||||
for (int i = 0; i < N; i++) {
|
||||
if (out_weight[i] == 0.0) dangling_sum += rank[i];
|
||||
}
|
||||
if (dangling_sum > 0.0) {
|
||||
double add = damping * dangling_sum / N;
|
||||
for (int i = 0; i < N; i++) new_rank[i] += add;
|
||||
}
|
||||
|
||||
/* Convergence: L2 norm of rank delta */
|
||||
double delta = 0.0;
|
||||
for (int i = 0; i < N; i++) {
|
||||
double d = new_rank[i] - rank[i];
|
||||
delta += d * d;
|
||||
}
|
||||
delta = sqrt(delta);
|
||||
|
||||
/* Swap buffers */
|
||||
double *tmp = rank; rank = new_rank; new_rank = tmp;
|
||||
|
||||
if (delta < epsilon) { iter++; break; }
|
||||
}
|
||||
|
||||
/* ── Step 5: Store PageRank in db ─────────────────────── */
|
||||
char ts[CBM_ISO_TIMESTAMP_LEN];
|
||||
iso_now(ts, sizeof(ts));
|
||||
|
||||
/* Clear old ranks for this scope */
|
||||
snprintf(sql_buf, sizeof(sql_buf), "DELETE FROM pagerank WHERE %s",
|
||||
scope_where(scope));
|
||||
if (sqlite3_prepare_v2(db, sql_buf, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
}
|
||||
|
||||
/* Batch insert within transaction */
|
||||
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
|
||||
const char *ins_sql =
|
||||
"INSERT OR REPLACE INTO pagerank "
|
||||
"(node_id, project, rank, computed_at) "
|
||||
"SELECT ?1, project, ?2, ?3 FROM nodes WHERE id = ?1";
|
||||
sqlite3_stmt *ins_stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, ins_sql, -1, &ins_stmt, NULL) == SQLITE_OK) {
|
||||
for (int i = 0; i < N; i++) {
|
||||
sqlite3_bind_int64(ins_stmt, 1, node_ids[i]);
|
||||
sqlite3_bind_double(ins_stmt, 2, rank[i]);
|
||||
sqlite3_bind_text(ins_stmt, 3, ts, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(ins_stmt);
|
||||
sqlite3_reset(ins_stmt);
|
||||
}
|
||||
sqlite3_finalize(ins_stmt);
|
||||
}
|
||||
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
||||
|
||||
/* ── Step 6: Compute LinkRank for edges ───────────────── */
|
||||
snprintf(sql_buf, sizeof(sql_buf), "DELETE FROM linkrank WHERE %s",
|
||||
scope_where(scope));
|
||||
if (sqlite3_prepare_v2(db, sql_buf, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
}
|
||||
|
||||
const char *lr_sql =
|
||||
"INSERT OR REPLACE INTO linkrank "
|
||||
"(edge_id, project, rank, computed_at) "
|
||||
"SELECT ?1, project, ?2, ?3 FROM edges WHERE id = ?1";
|
||||
sqlite3_stmt *lr_stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, lr_sql, -1, &lr_stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
|
||||
for (int e = 0; e < E; e++) {
|
||||
int s_idx = edges[e].src_idx;
|
||||
double lr = 0.0;
|
||||
if (out_weight[s_idx] > 0.0)
|
||||
lr = rank[s_idx] * edges[e].weight / out_weight[s_idx];
|
||||
sqlite3_bind_int64(lr_stmt, 1, edges[e].edge_id);
|
||||
sqlite3_bind_double(lr_stmt, 2, lr);
|
||||
sqlite3_bind_text(lr_stmt, 3, ts, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(lr_stmt);
|
||||
sqlite3_reset(lr_stmt);
|
||||
}
|
||||
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
||||
sqlite3_finalize(lr_stmt);
|
||||
}
|
||||
|
||||
/* ── Logging ──────────────────────────────────────────── */
|
||||
char iter_s[CBM_LOG_INT_BUF], n_s[CBM_LOG_INT_BUF], e_s[CBM_LOG_INT_BUF];
|
||||
snprintf(iter_s, sizeof(iter_s), "%d", iter);
|
||||
snprintf(n_s, sizeof(n_s), "%d", N);
|
||||
snprintf(e_s, sizeof(e_s), "%d", E);
|
||||
cbm_log_info("pagerank.done", "project", project,
|
||||
"nodes", n_s, "edges", e_s, "iterations", iter_s);
|
||||
|
||||
result = N;
|
||||
|
||||
cleanup:
|
||||
if (stmt) sqlite3_finalize(stmt); /* defensive: finalize any in-flight stmt */
|
||||
free(node_ids);
|
||||
id_map_free(&map);
|
||||
free(edges);
|
||||
free(out_weight);
|
||||
free(rank);
|
||||
free(new_rank);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cbm_pagerank_compute_default(cbm_store_t *store, const char *project) {
|
||||
return cbm_pagerank_compute(store, project,
|
||||
CBM_PAGERANK_DAMPING, CBM_PAGERANK_EPSILON,
|
||||
CBM_PAGERANK_MAX_ITER, &CBM_DEFAULT_EDGE_WEIGHTS,
|
||||
CBM_DEFAULT_RANK_SCOPE);
|
||||
}
|
||||
|
||||
double cbm_pagerank_get(cbm_store_t *store, int64_t node_id) {
|
||||
sqlite3 *db = cbm_store_get_db(store);
|
||||
if (!db) return 0.0;
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
double r = 0.0;
|
||||
if (sqlite3_prepare_v2(db, "SELECT rank FROM pagerank WHERE node_id = ?1",
|
||||
-1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, node_id);
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) r = sqlite3_column_double(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
double cbm_linkrank_get(cbm_store_t *store, int64_t edge_id) {
|
||||
sqlite3 *db = cbm_store_get_db(store);
|
||||
if (!db) return 0.0;
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
double r = 0.0;
|
||||
if (sqlite3_prepare_v2(db, "SELECT rank FROM linkrank WHERE edge_id = ?1",
|
||||
-1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, edge_id);
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) r = sqlite3_column_double(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/* pagerank.h — PageRank (node) + LinkRank (edge) ranking for codebase graphs.
|
||||
*
|
||||
* References:
|
||||
* - aider repomap (github.com/Aider-AI/aider/blob/main/aider/repomap.py)
|
||||
* - NetworkX pagerank (networkx/algorithms/link_analysis/pagerank_alg.py)
|
||||
* - RepoGraph (github.com/ozyyshr/RepoGraph) — peer-reviewed
|
||||
* - Kim et al. (2010) LinkRank, arXiv:0902.3728
|
||||
*/
|
||||
|
||||
#ifndef CBM_PAGERANK_H
|
||||
#define CBM_PAGERANK_H
|
||||
|
||||
#include <store/store.h>
|
||||
|
||||
/* ── Algorithm defaults (config-overridable) ──────────────── */
|
||||
|
||||
#define CBM_PAGERANK_DAMPING 0.85 /* Standard Google PageRank damping */
|
||||
#define CBM_PAGERANK_EPSILON 1e-6 /* L2 convergence threshold */
|
||||
#define CBM_PAGERANK_MAX_ITER 20 /* Max power iterations */
|
||||
|
||||
/* Config keys for runtime tuning */
|
||||
#define CBM_CONFIG_PAGERANK_MAX_ITER "pagerank_max_iter"
|
||||
#define CBM_CONFIG_RANK_SCOPE "rank_scope"
|
||||
|
||||
/* ── Internal tuning constants ────────────────────────────── */
|
||||
|
||||
#define CBM_PAGERANK_INITIAL_CAP 256 /* Initial array capacity for nodes/edges */
|
||||
#define CBM_ISO_TIMESTAMP_LEN 32 /* ISO-8601 timestamp buffer size */
|
||||
#define CBM_LOG_INT_BUF 16 /* int->string buffer for logging */
|
||||
#define CBM_HASHMAP_LOAD_FACTOR 2 /* Hash map capacity = N * factor + 1 */
|
||||
|
||||
/* ── Scope control ────────────────────────────────────────── */
|
||||
|
||||
typedef enum {
|
||||
CBM_RANK_SCOPE_PROJECT = 0, /* project nodes only */
|
||||
CBM_RANK_SCOPE_FULL = 1, /* project + all deps (default) */
|
||||
CBM_RANK_SCOPE_DEPS = 2, /* deps only */
|
||||
} cbm_rank_scope_t;
|
||||
|
||||
#define CBM_DEFAULT_RANK_SCOPE CBM_RANK_SCOPE_FULL
|
||||
|
||||
/* ── Edge type weights ────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
double calls; /* CALLS edges — direct function calls */
|
||||
double defines_method; /* DEFINES_METHOD — class->method */
|
||||
double defines; /* DEFINES — declaration->definition */
|
||||
double imports; /* IMPORTS — module imports */
|
||||
double usage; /* USAGE — variable/type references */
|
||||
double configures; /* CONFIGURES — config file links */
|
||||
double http_calls; /* HTTP_CALLS — cross-service */
|
||||
double async_calls; /* ASYNC_CALLS — async function calls */
|
||||
double default_weight; /* Fallback for unknown edge types */
|
||||
} cbm_edge_weights_t;
|
||||
|
||||
extern const cbm_edge_weights_t CBM_DEFAULT_EDGE_WEIGHTS;
|
||||
|
||||
/* ── PageRank API ─────────────────────────────────────────── */
|
||||
|
||||
/* Compute PageRank + LinkRank for all nodes/edges in a project scope.
|
||||
* Stores results in pagerank and linkrank tables.
|
||||
* Called after index_repository dump/flush.
|
||||
*
|
||||
* Runtime: O(max_iter * (V + E)), typically 20 * (V + E).
|
||||
* Memory: O(V) for rank arrays + O(E) for edge list.
|
||||
* Returns: number of nodes ranked, or -1 on error. */
|
||||
int cbm_pagerank_compute(cbm_store_t *store, const char *project,
|
||||
double damping, double epsilon, int max_iter,
|
||||
const cbm_edge_weights_t *weights,
|
||||
cbm_rank_scope_t scope);
|
||||
|
||||
/* Convenience: compute with defaults (FULL scope, d=0.85, eps=1e-6, 20 iter) */
|
||||
int cbm_pagerank_compute_default(cbm_store_t *store, const char *project);
|
||||
|
||||
/* Get PageRank score for a single node. Returns 0.0 if not computed. */
|
||||
double cbm_pagerank_get(cbm_store_t *store, int64_t node_id);
|
||||
|
||||
/* ── LinkRank API ─────────────────────────────────────────── */
|
||||
|
||||
/* Get LinkRank score for a single edge. Returns 0.0 if not computed. */
|
||||
double cbm_linkrank_get(cbm_store_t *store, int64_t edge_id);
|
||||
|
||||
#endif /* CBM_PAGERANK_H */
|
||||
+88
-24
@@ -73,6 +73,12 @@ struct cbm_store {
|
||||
sqlite3_stmt *stmt_delete_file_hashes;
|
||||
};
|
||||
|
||||
/* ── Public accessor ────────────────────────────────────────────── */
|
||||
|
||||
sqlite3 *cbm_store_get_db(cbm_store_t *s) {
|
||||
return s ? s->db : NULL;
|
||||
}
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────────── */
|
||||
|
||||
static void store_set_error(cbm_store_t *s, const char *msg) {
|
||||
@@ -195,6 +201,18 @@ static int init_schema(cbm_store_t *s) {
|
||||
" source_hash TEXT NOT NULL,"
|
||||
" created_at TEXT NOT NULL,"
|
||||
" updated_at TEXT NOT NULL"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS pagerank ("
|
||||
" node_id INTEGER PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,"
|
||||
" project TEXT NOT NULL,"
|
||||
" rank REAL NOT NULL DEFAULT 0.0,"
|
||||
" computed_at TEXT NOT NULL"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS linkrank ("
|
||||
" edge_id INTEGER PRIMARY KEY REFERENCES edges(id) ON DELETE CASCADE,"
|
||||
" project TEXT NOT NULL,"
|
||||
" rank REAL NOT NULL DEFAULT 0.0,"
|
||||
" computed_at TEXT NOT NULL"
|
||||
");";
|
||||
|
||||
return exec_sql(s, ddl);
|
||||
@@ -209,7 +227,10 @@ static int create_user_indexes(cbm_store_t *s) {
|
||||
"CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, type);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(project, type);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(project, target_id, type);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(project, source_id, type);";
|
||||
"CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(project, source_id, type);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_pagerank_project ON pagerank(project);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_pagerank_rank ON pagerank(project, rank DESC);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_linkrank_project ON linkrank(project);";
|
||||
return exec_sql(s, sql);
|
||||
}
|
||||
|
||||
@@ -1734,12 +1755,25 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
char count_sql[4096];
|
||||
int bind_idx = 0;
|
||||
|
||||
/* We build a query that selects nodes with optional degree subqueries */
|
||||
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 = 'CALLS') AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND e.type = 'CALLS') AS out_deg ";
|
||||
/* 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 *select_cols;
|
||||
if (use_pagerank) {
|
||||
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 = 'CALLS') AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND e.type = 'CALLS') AS out_deg, "
|
||||
"COALESCE(pr.rank, 0.0) AS pr_rank ";
|
||||
} else {
|
||||
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 = 'CALLS') AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND e.type = 'CALLS') AS out_deg ";
|
||||
}
|
||||
|
||||
/* Start building WHERE */
|
||||
char where[2048] = "";
|
||||
@@ -1825,10 +1859,13 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
}
|
||||
|
||||
/* Build full SQL */
|
||||
const char *from_join = use_pagerank
|
||||
? "FROM nodes n LEFT JOIN pagerank pr ON pr.node_id = n.id"
|
||||
: "FROM nodes n";
|
||||
if (nparams > 0) {
|
||||
snprintf(sql, sizeof(sql), "%s FROM nodes n WHERE %s", select_cols, where);
|
||||
snprintf(sql, sizeof(sql), "%s %s WHERE %s", select_cols, from_join, where);
|
||||
} else {
|
||||
snprintf(sql, sizeof(sql), "%s FROM nodes n", select_cols);
|
||||
snprintf(sql, sizeof(sql), "%s %s", select_cols, from_join);
|
||||
}
|
||||
|
||||
/* Degree filters: -1 = no filter, 0+ = active filter.
|
||||
@@ -1863,19 +1900,40 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
|
||||
const char *name_col = has_degree_wrap ? "name" : "n.name";
|
||||
char order_limit[128];
|
||||
/* Stable pagination: ORDER BY name, id prevents duplicates across pages.
|
||||
* When project_pattern includes deps, add project-first sort so project
|
||||
* results appear before dependency results. */
|
||||
/* Sort dispatch: relevance (PageRank), name, degree.
|
||||
* Stable pagination via secondary sort on name, id. */
|
||||
const char *id_col = has_degree_wrap ? "id" : "n.id";
|
||||
if (params->project_pattern && !params->sort_by) {
|
||||
const char *proj_col = has_degree_wrap ? "project" : "n.project";
|
||||
const char *pr_col = has_degree_wrap ? "pr_rank" : "pr_rank";
|
||||
if (use_pagerank) {
|
||||
/* Relevance sort: PageRank DESC, then dep-last, then name for stability */
|
||||
if (params->project_pattern) {
|
||||
const char *proj_col = has_degree_wrap ? "project" : "n.project";
|
||||
snprintf(order_limit, sizeof(order_limit),
|
||||
" ORDER BY %s DESC, "
|
||||
"CASE WHEN %s LIKE '%%.dep.%%' THEN 1 ELSE 0 END, %s, %s"
|
||||
" LIMIT %d OFFSET %d",
|
||||
pr_col, proj_col, name_col, id_col, limit, offset);
|
||||
} else {
|
||||
snprintf(order_limit, sizeof(order_limit),
|
||||
" ORDER BY %s DESC, %s, %s LIMIT %d OFFSET %d",
|
||||
pr_col, name_col, id_col, limit, offset);
|
||||
}
|
||||
} else if (params->sort_by && strcmp(params->sort_by, "degree") == 0) {
|
||||
snprintf(order_limit, sizeof(order_limit),
|
||||
" ORDER BY CASE WHEN %s LIKE '%%.dep.%%' THEN 1 ELSE 0 END, %s, %s"
|
||||
" LIMIT %d OFFSET %d",
|
||||
proj_col, name_col, id_col, limit, offset);
|
||||
} else {
|
||||
snprintf(order_limit, sizeof(order_limit), " ORDER BY %s, %s LIMIT %d OFFSET %d",
|
||||
" ORDER BY (in_deg + out_deg) DESC, %s, %s LIMIT %d OFFSET %d",
|
||||
name_col, id_col, limit, offset);
|
||||
} else {
|
||||
/* name sort (explicit or fallback) */
|
||||
if (params->project_pattern) {
|
||||
const char *proj_col = has_degree_wrap ? "project" : "n.project";
|
||||
snprintf(order_limit, sizeof(order_limit),
|
||||
" ORDER BY CASE WHEN %s LIKE '%%.dep.%%' THEN 1 ELSE 0 END, %s, %s"
|
||||
" LIMIT %d OFFSET %d",
|
||||
proj_col, name_col, id_col, limit, offset);
|
||||
} else {
|
||||
snprintf(order_limit, sizeof(order_limit), " ORDER BY %s, %s LIMIT %d OFFSET %d",
|
||||
name_col, id_col, limit, offset);
|
||||
}
|
||||
}
|
||||
strncat(sql, order_limit, sizeof(sql) - strlen(sql) - 1);
|
||||
|
||||
@@ -1918,6 +1976,7 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
scan_node(main_stmt, &results[n].node);
|
||||
results[n].in_degree = sqlite3_column_int(main_stmt, 9);
|
||||
results[n].out_degree = sqlite3_column_int(main_stmt, 10);
|
||||
results[n].pagerank_score = use_pagerank ? sqlite3_column_double(main_stmt, 11) : 0.0;
|
||||
n++;
|
||||
}
|
||||
|
||||
@@ -2004,11 +2063,13 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
|
||||
" WHERE e.type IN (%s) AND bfs.hop < %d"
|
||||
")"
|
||||
"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 "
|
||||
"n.file_path, n.start_line, n.end_line, n.properties, bfs.hop, "
|
||||
"COALESCE(pr.rank, 0.0) AS pr_rank "
|
||||
"FROM bfs "
|
||||
"JOIN nodes n ON n.id = bfs.node_id "
|
||||
"LEFT JOIN pagerank pr ON pr.node_id = n.id "
|
||||
"WHERE bfs.hop > 0 " /* exclude root */
|
||||
"ORDER BY bfs.hop "
|
||||
"ORDER BY bfs.hop, pr_rank DESC "
|
||||
"LIMIT %d;",
|
||||
(long long)start_id, next_id, join_cond, types_clause, max_depth, max_results);
|
||||
|
||||
@@ -2050,12 +2111,15 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
|
||||
|
||||
char edge_sql[8192];
|
||||
snprintf(edge_sql, sizeof(edge_sql),
|
||||
"SELECT n1.name, n2.name, e.type "
|
||||
"SELECT n1.name, n2.name, e.type, "
|
||||
"COALESCE(lr.rank, 0.0) AS lr_rank "
|
||||
"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 "
|
||||
"WHERE e.source_id IN (%s) AND e.target_id IN (%s) "
|
||||
"AND e.type IN (%s)",
|
||||
"AND e.type IN (%s) "
|
||||
"ORDER BY lr_rank DESC",
|
||||
id_set, id_set, types_clause);
|
||||
|
||||
sqlite3_stmt *estmt = NULL;
|
||||
@@ -2073,7 +2137,7 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const
|
||||
edges[en].from_name = heap_strdup((const char *)sqlite3_column_text(estmt, 0));
|
||||
edges[en].to_name = heap_strdup((const char *)sqlite3_column_text(estmt, 1));
|
||||
edges[en].type = heap_strdup((const char *)sqlite3_column_text(estmt, 2));
|
||||
edges[en].confidence = 1.0;
|
||||
edges[en].confidence = sqlite3_column_double(estmt, 3);
|
||||
en++;
|
||||
}
|
||||
sqlite3_finalize(estmt);
|
||||
|
||||
@@ -123,6 +123,7 @@ typedef struct {
|
||||
cbm_node_t node;
|
||||
int in_degree;
|
||||
int out_degree;
|
||||
double pagerank_score; /* PageRank rank, 0.0 if not computed */
|
||||
/* connected_names: allocated array of strings, count in connected_count */
|
||||
const char **connected_names;
|
||||
int connected_count;
|
||||
@@ -201,6 +202,10 @@ void cbm_store_close(cbm_store_t *s);
|
||||
/* Get the last error message (static string, valid until next call). */
|
||||
const char *cbm_store_error(cbm_store_t *s);
|
||||
|
||||
/* Raw SQLite handle — use for pagerank/linkrank bulk inserts.
|
||||
* Do NOT use for schema modifications. Returns NULL if store is NULL. */
|
||||
struct sqlite3 *cbm_store_get_db(cbm_store_t *s);
|
||||
|
||||
/* ── Transaction ────────────────────────────────────────────────── */
|
||||
|
||||
/* Begin a transaction. Returns CBM_STORE_OK on success. */
|
||||
|
||||
@@ -819,7 +819,6 @@ TEST(test_index_status_shows_deps) {
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(test_trace_results_have_source_field) {
|
||||
/* trace_call_path results for project nodes must have source:"project" */
|
||||
char tmp[256];
|
||||
cbm_mcp_server_t *srv = setup_dep_query_server(tmp, sizeof(tmp));
|
||||
ASSERT_NOT_NULL(srv);
|
||||
@@ -831,7 +830,6 @@ TEST(test_trace_results_have_source_field) {
|
||||
free(raw);
|
||||
ASSERT_NOT_NULL(resp);
|
||||
|
||||
/* Callees should have source field tagged as "project" */
|
||||
if (strstr(resp, "callees") && strstr(resp, "source")) {
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"source\":\"project\""));
|
||||
}
|
||||
@@ -843,7 +841,6 @@ TEST(test_trace_results_have_source_field) {
|
||||
}
|
||||
|
||||
TEST(test_snippet_has_source_field) {
|
||||
/* get_code_snippet results must have source field */
|
||||
char tmp[256];
|
||||
cbm_mcp_server_t *srv = setup_dep_query_server(tmp, sizeof(tmp));
|
||||
ASSERT_NOT_NULL(srv);
|
||||
@@ -855,7 +852,6 @@ TEST(test_snippet_has_source_field) {
|
||||
free(raw);
|
||||
ASSERT_NOT_NULL(resp);
|
||||
|
||||
/* Project snippet must have source:"project" */
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"source\":\"project\""));
|
||||
|
||||
free(resp);
|
||||
@@ -865,11 +861,9 @@ TEST(test_snippet_has_source_field) {
|
||||
}
|
||||
|
||||
TEST(test_cross_edges_null_safety) {
|
||||
/* cbm_dep_link_cross_edges must handle NULL/empty args safely */
|
||||
ASSERT_EQ(0, cbm_dep_link_cross_edges(NULL, "test"));
|
||||
ASSERT_EQ(0, cbm_dep_link_cross_edges(NULL, NULL));
|
||||
|
||||
/* With a valid store but no deps, should return 0 (no edges linked) */
|
||||
cbm_store_t *st = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(st);
|
||||
ASSERT_EQ(0, cbm_dep_link_cross_edges(st, "nonexistent"));
|
||||
|
||||
@@ -49,6 +49,7 @@ extern void suite_mem(void);
|
||||
extern void suite_ui(void);
|
||||
extern void suite_token_reduction(void);
|
||||
extern void suite_depindex(void);
|
||||
extern void suite_pagerank(void);
|
||||
extern void suite_integration(void);
|
||||
|
||||
int main(void) {
|
||||
@@ -138,6 +139,9 @@ int main(void) {
|
||||
/* Dependency indexing */
|
||||
RUN_SUITE(depindex);
|
||||
|
||||
/* PageRank (node + edge ranking) */
|
||||
RUN_SUITE(pagerank);
|
||||
|
||||
/* Integration (end-to-end) */
|
||||
RUN_SUITE(integration);
|
||||
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
/*
|
||||
* test_pagerank.c — Tests for PageRank (node) + LinkRank (edge) ranking.
|
||||
*
|
||||
* TDD: All tests written BEFORE implementation. They should fail (RED)
|
||||
* until the corresponding feature is implemented (GREEN).
|
||||
*
|
||||
* References:
|
||||
* - igraph test suite: pagerank, multigraph, dangling, complete graph
|
||||
* - NetworkX test suite: test_pagerank, test_dangling, test_empty
|
||||
* - aider repomap: edge weights, file rank distribution
|
||||
* - Kim et al. (2010) LinkRank: edge ranking formula
|
||||
*/
|
||||
#include "../src/foundation/compat.h"
|
||||
#include "test_framework.h"
|
||||
#include <store/store.h>
|
||||
#include <pagerank/pagerank.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
/* ── Test helpers ──────────────────────────────────────────── */
|
||||
|
||||
static int64_t add_node(cbm_store_t *s, const char *project, const char *name) {
|
||||
cbm_node_t n = {0};
|
||||
n.project = project;
|
||||
n.label = "Function";
|
||||
n.name = name;
|
||||
n.qualified_name = name;
|
||||
n.file_path = "test.c";
|
||||
return cbm_store_upsert_node(s, &n);
|
||||
}
|
||||
|
||||
static int64_t add_edge(cbm_store_t *s, const char *project,
|
||||
int64_t src, int64_t dst, const char *type) {
|
||||
cbm_edge_t e = {0};
|
||||
e.project = project;
|
||||
e.source_id = src;
|
||||
e.target_id = dst;
|
||||
e.type = type;
|
||||
return cbm_store_insert_edge(s, &e);
|
||||
}
|
||||
|
||||
static double get_pr(cbm_store_t *s, int64_t node_id) {
|
||||
return cbm_pagerank_get(s, node_id);
|
||||
}
|
||||
|
||||
static int count_table_rows(cbm_store_t *s, const char *table) {
|
||||
sqlite3 *db = cbm_store_get_db(s);
|
||||
if (!db) return -1;
|
||||
char sql[64];
|
||||
snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM %s", table);
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int count = 0;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static double get_lr_by_edge_id(cbm_store_t *s, int64_t edge_id) {
|
||||
return cbm_linkrank_get(s, edge_id);
|
||||
}
|
||||
|
||||
/* ── 1. Core PageRank tests ──────────────────────────────── */
|
||||
|
||||
TEST(pagerank_empty_graph) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "empty", "/tmp/empty");
|
||||
int rc = cbm_pagerank_compute_default(s, "empty");
|
||||
ASSERT_EQ(rc, 0); /* 0 nodes ranked */
|
||||
ASSERT_EQ(count_table_rows(s, "pagerank"), 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_single_node) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "single", "/tmp/single");
|
||||
int64_t a = add_node(s, "single", "main");
|
||||
int rc = cbm_pagerank_compute_default(s, "single");
|
||||
ASSERT_EQ(rc, 1);
|
||||
double r = get_pr(s, a);
|
||||
ASSERT_TRUE(fabs(r - 1.0) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_two_nodes_one_edge) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "two", "/tmp/two");
|
||||
int64_t a = add_node(s, "two", "caller");
|
||||
int64_t b = add_node(s, "two", "callee");
|
||||
add_edge(s, "two", a, b, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "two");
|
||||
double ra = get_pr(s, a);
|
||||
double rb = get_pr(s, b);
|
||||
ASSERT_TRUE(rb > ra); /* callee gets more rank */
|
||||
ASSERT_TRUE(fabs(ra + rb - 1.0) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_cycle) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "cyc", "/tmp/cyc");
|
||||
int64_t a = add_node(s, "cyc", "funcA");
|
||||
int64_t b = add_node(s, "cyc", "funcB");
|
||||
add_edge(s, "cyc", a, b, "CALLS");
|
||||
add_edge(s, "cyc", b, a, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "cyc");
|
||||
double ra = get_pr(s, a);
|
||||
double rb = get_pr(s, b);
|
||||
ASSERT_TRUE(fabs(ra - rb) < 0.01); /* symmetric */
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_star_topology) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "star", "/tmp/star");
|
||||
int64_t hub = add_node(s, "star", "hub");
|
||||
int64_t s1 = add_node(s, "star", "spoke1");
|
||||
int64_t s2 = add_node(s, "star", "spoke2");
|
||||
int64_t s3 = add_node(s, "star", "spoke3");
|
||||
add_edge(s, "star", s1, hub, "CALLS");
|
||||
add_edge(s, "star", s2, hub, "CALLS");
|
||||
add_edge(s, "star", s3, hub, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "star");
|
||||
ASSERT_TRUE(get_pr(s, hub) > get_pr(s, s1));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_edge_weights) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "wt", "/tmp/wt");
|
||||
int64_t a = add_node(s, "wt", "source");
|
||||
int64_t b = add_node(s, "wt", "called");
|
||||
int64_t c = add_node(s, "wt", "used");
|
||||
add_edge(s, "wt", a, b, "CALLS"); /* weight 1.0 */
|
||||
add_edge(s, "wt", a, c, "USAGE"); /* weight 0.2 */
|
||||
cbm_pagerank_compute_default(s, "wt");
|
||||
ASSERT_TRUE(get_pr(s, b) > get_pr(s, c));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_convergence) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "chain", "/tmp/chain");
|
||||
int64_t ids[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char name[8]; snprintf(name, sizeof(name), "n%d", i);
|
||||
ids[i] = add_node(s, "chain", name);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) add_edge(s, "chain", ids[i], ids[i+1], "CALLS");
|
||||
int rc = cbm_pagerank_compute_default(s, "chain");
|
||||
ASSERT_EQ(rc, 5);
|
||||
ASSERT_TRUE(get_pr(s, ids[4]) > get_pr(s, ids[0]));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_sum_to_one) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "sum", "/tmp/sum");
|
||||
int64_t a = add_node(s, "sum", "a");
|
||||
int64_t b = add_node(s, "sum", "b");
|
||||
int64_t c = add_node(s, "sum", "c");
|
||||
add_edge(s, "sum", a, b, "CALLS");
|
||||
add_edge(s, "sum", b, c, "CALLS");
|
||||
add_edge(s, "sum", c, a, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "sum");
|
||||
double total = get_pr(s, a) + get_pr(s, b) + get_pr(s, c);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_stored_in_db) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "db", "/tmp/db");
|
||||
add_node(s, "db", "f1");
|
||||
add_node(s, "db", "f2");
|
||||
cbm_pagerank_compute_default(s, "db");
|
||||
ASSERT_EQ(count_table_rows(s, "pagerank"), 2);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_recompute_replaces) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "re", "/tmp/re");
|
||||
int64_t a = add_node(s, "re", "f1");
|
||||
cbm_pagerank_compute_default(s, "re");
|
||||
double r1 = get_pr(s, a);
|
||||
cbm_pagerank_compute_default(s, "re");
|
||||
ASSERT_EQ(count_table_rows(s, "pagerank"), 1);
|
||||
double r2 = get_pr(s, a);
|
||||
ASSERT_TRUE(fabs(r1 - r2) < 0.001);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_full_scope_includes_deps) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "proj", "/tmp/proj");
|
||||
cbm_store_upsert_project(s, "proj.dep.lib", "/tmp/lib");
|
||||
int64_t a = add_node(s, "proj", "app_main");
|
||||
int64_t b = add_node(s, "proj.dep.lib", "lib_func");
|
||||
add_edge(s, "proj", a, b, "CALLS");
|
||||
int rc = cbm_pagerank_compute(s, "proj", CBM_PAGERANK_DAMPING,
|
||||
CBM_PAGERANK_EPSILON, CBM_PAGERANK_MAX_ITER,
|
||||
&CBM_DEFAULT_EDGE_WEIGHTS, CBM_RANK_SCOPE_FULL);
|
||||
ASSERT_EQ(rc, 2);
|
||||
ASSERT_TRUE(get_pr(s, b) > 0.0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_project_scope_excludes_deps) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "proj2", "/tmp/proj2");
|
||||
cbm_store_upsert_project(s, "proj2.dep.lib", "/tmp/lib2");
|
||||
add_node(s, "proj2", "my_func");
|
||||
int64_t dep = add_node(s, "proj2.dep.lib", "lib_func");
|
||||
int rc = cbm_pagerank_compute(s, "proj2", CBM_PAGERANK_DAMPING,
|
||||
CBM_PAGERANK_EPSILON, CBM_PAGERANK_MAX_ITER,
|
||||
&CBM_DEFAULT_EDGE_WEIGHTS, CBM_RANK_SCOPE_PROJECT);
|
||||
ASSERT_EQ(rc, 1);
|
||||
ASSERT_TRUE(get_pr(s, dep) == 0.0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_dangling_nodes) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "dang", "/tmp/dang");
|
||||
int64_t a = add_node(s, "dang", "caller");
|
||||
int64_t b = add_node(s, "dang", "leaf");
|
||||
add_edge(s, "dang", a, b, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "dang");
|
||||
ASSERT_TRUE(get_pr(s, b) > 0.0);
|
||||
double total = get_pr(s, a) + get_pr(s, b);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_null_safety) {
|
||||
ASSERT_EQ(cbm_pagerank_compute_default(NULL, "x"), -1);
|
||||
ASSERT_EQ(cbm_pagerank_compute_default(NULL, NULL), -1);
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_EQ(cbm_pagerank_compute_default(s, NULL), -1);
|
||||
ASSERT_EQ(cbm_pagerank_compute_default(s, ""), -1);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 2. Edge cases from igraph/NetworkX ──────────────────── */
|
||||
|
||||
TEST(pagerank_self_loop) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "self", "/tmp/self");
|
||||
int64_t a = add_node(s, "self", "recursive");
|
||||
add_edge(s, "self", a, a, "CALLS");
|
||||
int rc = cbm_pagerank_compute_default(s, "self");
|
||||
ASSERT_EQ(rc, 1);
|
||||
ASSERT_TRUE(fabs(get_pr(s, a) - 1.0) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_disconnected_components) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "disc", "/tmp/disc");
|
||||
int64_t a = add_node(s, "disc", "a");
|
||||
int64_t b = add_node(s, "disc", "b");
|
||||
int64_t c = add_node(s, "disc", "c");
|
||||
int64_t d = add_node(s, "disc", "d");
|
||||
add_edge(s, "disc", a, b, "CALLS");
|
||||
add_edge(s, "disc", c, d, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "disc");
|
||||
double total = get_pr(s, a) + get_pr(s, b) + get_pr(s, c) + get_pr(s, d);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
double comp1 = get_pr(s, a) + get_pr(s, b);
|
||||
double comp2 = get_pr(s, c) + get_pr(s, d);
|
||||
ASSERT_TRUE(fabs(comp1 - comp2) < 0.15);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_all_dangling_no_edges) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "noedge", "/tmp/noedge");
|
||||
int64_t ids[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char name[16]; snprintf(name, sizeof(name), "n%d", i);
|
||||
ids[i] = add_node(s, "noedge", name);
|
||||
}
|
||||
int rc = cbm_pagerank_compute_default(s, "noedge");
|
||||
ASSERT_EQ(rc, 5);
|
||||
double expected = 1.0 / 5.0;
|
||||
for (int i = 0; i < 5; i++)
|
||||
ASSERT_TRUE(fabs(get_pr(s, ids[i]) - expected) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_complete_graph) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "kn", "/tmp/kn");
|
||||
int64_t ids[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
char name[8]; snprintf(name, sizeof(name), "k%d", i);
|
||||
ids[i] = add_node(s, "kn", name);
|
||||
}
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (int j = 0; j < 4; j++)
|
||||
if (i != j) add_edge(s, "kn", ids[i], ids[j], "CALLS");
|
||||
cbm_pagerank_compute_default(s, "kn");
|
||||
for (int i = 0; i < 4; i++)
|
||||
ASSERT_TRUE(fabs(get_pr(s, ids[i]) - 0.25) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_multigraph_edges) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "multi", "/tmp/multi");
|
||||
int64_t a = add_node(s, "multi", "caller");
|
||||
int64_t b = add_node(s, "multi", "callee");
|
||||
add_edge(s, "multi", a, b, "CALLS");
|
||||
add_edge(s, "multi", a, b, "IMPORTS");
|
||||
cbm_pagerank_compute_default(s, "multi");
|
||||
ASSERT_TRUE(get_pr(s, b) > get_pr(s, a));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_large_graph_stability) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "big", "/tmp/big");
|
||||
int64_t ids[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char name[16]; snprintf(name, sizeof(name), "f%d", i);
|
||||
ids[i] = add_node(s, "big", name);
|
||||
}
|
||||
for (int i = 0; i < 99; i++)
|
||||
add_edge(s, "big", ids[i], ids[i+1], "CALLS");
|
||||
int rc = cbm_pagerank_compute_default(s, "big");
|
||||
ASSERT_EQ(rc, 100);
|
||||
double total = 0.0;
|
||||
for (int i = 0; i < 100; i++) total += get_pr(s, ids[i]);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
ASSERT_TRUE(get_pr(s, ids[99]) > get_pr(s, ids[0]));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_zero_weight_edges) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "zw", "/tmp/zw");
|
||||
int64_t a = add_node(s, "zw", "a");
|
||||
int64_t b = add_node(s, "zw", "b");
|
||||
add_edge(s, "zw", a, b, "CONFIGURES");
|
||||
cbm_edge_weights_t zero_w = CBM_DEFAULT_EDGE_WEIGHTS;
|
||||
zero_w.configures = 0.0;
|
||||
cbm_pagerank_compute(s, "zw", CBM_PAGERANK_DAMPING, CBM_PAGERANK_EPSILON,
|
||||
CBM_PAGERANK_MAX_ITER, &zero_w, CBM_RANK_SCOPE_FULL);
|
||||
ASSERT_TRUE(fabs(get_pr(s, a) - get_pr(s, b)) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_custom_damping_high) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "hi_d", "/tmp/hi_d");
|
||||
int64_t a = add_node(s, "hi_d", "a");
|
||||
int64_t b = add_node(s, "hi_d", "b");
|
||||
add_edge(s, "hi_d", a, b, "CALLS");
|
||||
cbm_pagerank_compute(s, "hi_d", 0.99, CBM_PAGERANK_EPSILON,
|
||||
50, &CBM_DEFAULT_EDGE_WEIGHTS, CBM_RANK_SCOPE_FULL);
|
||||
double total = get_pr(s, a) + get_pr(s, b);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
ASSERT_TRUE(get_pr(s, b) > get_pr(s, a));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_custom_damping_low) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lo_d", "/tmp/lo_d");
|
||||
int64_t a = add_node(s, "lo_d", "a");
|
||||
int64_t b = add_node(s, "lo_d", "b");
|
||||
add_edge(s, "lo_d", a, b, "CALLS");
|
||||
cbm_pagerank_compute(s, "lo_d", 0.1, CBM_PAGERANK_EPSILON,
|
||||
CBM_PAGERANK_MAX_ITER, &CBM_DEFAULT_EDGE_WEIGHTS,
|
||||
CBM_RANK_SCOPE_FULL);
|
||||
ASSERT_TRUE(fabs(get_pr(s, a) - get_pr(s, b)) < 0.1);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_max_iter_zero) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "mi0", "/tmp/mi0");
|
||||
add_node(s, "mi0", "a");
|
||||
add_node(s, "mi0", "b");
|
||||
add_edge(s, "mi0", 1, 2, "CALLS");
|
||||
/* max_iter <= 0 resets to default */
|
||||
int rc = cbm_pagerank_compute(s, "mi0", CBM_PAGERANK_DAMPING,
|
||||
CBM_PAGERANK_EPSILON, 0,
|
||||
&CBM_DEFAULT_EDGE_WEIGHTS, CBM_RANK_SCOPE_FULL);
|
||||
ASSERT_TRUE(rc > 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_known_values) {
|
||||
/* 3-node cycle: all should get equal rank 1/3 */
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "kv", "/tmp/kv");
|
||||
int64_t a = add_node(s, "kv", "a");
|
||||
int64_t b = add_node(s, "kv", "b");
|
||||
int64_t c = add_node(s, "kv", "c");
|
||||
add_edge(s, "kv", a, b, "CALLS");
|
||||
add_edge(s, "kv", b, c, "CALLS");
|
||||
add_edge(s, "kv", c, a, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "kv");
|
||||
double expected = 1.0 / 3.0;
|
||||
ASSERT_TRUE(fabs(get_pr(s, a) - expected) < 0.01);
|
||||
ASSERT_TRUE(fabs(get_pr(s, b) - expected) < 0.01);
|
||||
ASSERT_TRUE(fabs(get_pr(s, c) - expected) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_known_values_asymmetric) {
|
||||
/* NetworkX test graph: 6 nodes, node 4 highest rank */
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "nx", "/tmp/nx");
|
||||
int64_t n[7];
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
char name[8]; snprintf(name, sizeof(name), "n%d", i);
|
||||
n[i] = add_node(s, "nx", name);
|
||||
}
|
||||
add_edge(s, "nx", n[1], n[2], "CALLS");
|
||||
add_edge(s, "nx", n[1], n[3], "CALLS");
|
||||
add_edge(s, "nx", n[3], n[1], "CALLS");
|
||||
add_edge(s, "nx", n[3], n[2], "CALLS");
|
||||
add_edge(s, "nx", n[3], n[5], "CALLS");
|
||||
add_edge(s, "nx", n[4], n[5], "CALLS");
|
||||
add_edge(s, "nx", n[4], n[6], "CALLS");
|
||||
add_edge(s, "nx", n[5], n[4], "CALLS");
|
||||
add_edge(s, "nx", n[5], n[6], "CALLS");
|
||||
add_edge(s, "nx", n[6], n[4], "CALLS");
|
||||
cbm_pagerank_compute_default(s, "nx");
|
||||
ASSERT_TRUE(get_pr(s, n[4]) > get_pr(s, n[1]));
|
||||
ASSERT_TRUE(get_pr(s, n[2]) > 0.0); /* dangling node gets rank */
|
||||
double total = 0;
|
||||
for (int i = 1; i <= 6; i++) total += get_pr(s, n[i]);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pagerank_scope_deps_only) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "sd", "/tmp/sd");
|
||||
cbm_store_upsert_project(s, "sd.dep.lib", "/tmp/sdlib");
|
||||
int64_t proj_node = add_node(s, "sd", "app");
|
||||
int64_t dep_node = add_node(s, "sd.dep.lib", "lib");
|
||||
int rc = cbm_pagerank_compute(s, "sd", CBM_PAGERANK_DAMPING,
|
||||
CBM_PAGERANK_EPSILON, CBM_PAGERANK_MAX_ITER,
|
||||
&CBM_DEFAULT_EDGE_WEIGHTS, CBM_RANK_SCOPE_DEPS);
|
||||
ASSERT_EQ(rc, 1);
|
||||
ASSERT_TRUE(get_pr(s, dep_node) > 0.0);
|
||||
ASSERT_TRUE(get_pr(s, proj_node) == 0.0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 3. LinkRank tests ───────────────────────────────────── */
|
||||
|
||||
TEST(linkrank_computed_from_pagerank) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lr", "/tmp/lr");
|
||||
add_node(s, "lr", "f1");
|
||||
add_node(s, "lr", "f2");
|
||||
add_edge(s, "lr", 1, 2, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "lr");
|
||||
ASSERT_TRUE(count_table_rows(s, "linkrank") > 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_formula_correct) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrf", "/tmp/lrf");
|
||||
int64_t a = add_node(s, "lrf", "src");
|
||||
int64_t b = add_node(s, "lrf", "dst");
|
||||
int64_t eid = add_edge(s, "lrf", a, b, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "lrf");
|
||||
double pra = get_pr(s, a);
|
||||
double lr = get_lr_by_edge_id(s, eid);
|
||||
/* Single outgoing CALLS (weight 1.0): LR = PR(A) * 1.0 / 1.0 = PR(A) */
|
||||
ASSERT_TRUE(fabs(lr - pra) < 0.01);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_calls_higher_than_usage) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrw", "/tmp/lrw");
|
||||
int64_t a = add_node(s, "lrw", "src");
|
||||
int64_t b = add_node(s, "lrw", "called");
|
||||
int64_t c = add_node(s, "lrw", "used");
|
||||
int64_t e1 = add_edge(s, "lrw", a, b, "CALLS");
|
||||
int64_t e2 = add_edge(s, "lrw", a, c, "USAGE");
|
||||
cbm_pagerank_compute_default(s, "lrw");
|
||||
ASSERT_TRUE(get_lr_by_edge_id(s, e1) > get_lr_by_edge_id(s, e2));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_stored_in_db) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrs", "/tmp/lrs");
|
||||
add_node(s, "lrs", "f1");
|
||||
add_node(s, "lrs", "f2");
|
||||
add_edge(s, "lrs", 1, 2, "CALLS");
|
||||
add_edge(s, "lrs", 2, 1, "IMPORTS");
|
||||
cbm_pagerank_compute_default(s, "lrs");
|
||||
ASSERT_EQ(count_table_rows(s, "linkrank"), 2);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_self_loop_edge) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrsl", "/tmp/lrsl");
|
||||
int64_t a = add_node(s, "lrsl", "recursive");
|
||||
int64_t eid = add_edge(s, "lrsl", a, a, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "lrsl");
|
||||
ASSERT_EQ(count_table_rows(s, "linkrank"), 1);
|
||||
ASSERT_TRUE(get_lr_by_edge_id(s, eid) > 0.0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_no_edges) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrne", "/tmp/lrne");
|
||||
add_node(s, "lrne", "isolated");
|
||||
cbm_pagerank_compute_default(s, "lrne");
|
||||
ASSERT_EQ(count_table_rows(s, "linkrank"), 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(linkrank_sum_equals_pagerank_sum) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "lrs2", "/tmp/lrs2");
|
||||
int64_t a = add_node(s, "lrs2", "a");
|
||||
int64_t b = add_node(s, "lrs2", "b");
|
||||
int64_t c = add_node(s, "lrs2", "c");
|
||||
add_edge(s, "lrs2", a, b, "CALLS");
|
||||
add_edge(s, "lrs2", b, c, "CALLS");
|
||||
add_edge(s, "lrs2", c, a, "CALLS");
|
||||
cbm_pagerank_compute_default(s, "lrs2");
|
||||
sqlite3 *db = cbm_store_get_db(s);
|
||||
sqlite3_stmt *st = NULL;
|
||||
double lr_sum = 0.0;
|
||||
sqlite3_prepare_v2(db, "SELECT SUM(rank) FROM linkrank", -1, &st, NULL);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) lr_sum = sqlite3_column_double(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
double pr_sum = get_pr(s, a) + get_pr(s, b) + get_pr(s, c);
|
||||
ASSERT_TRUE(fabs(lr_sum - pr_sum) < 0.05);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 4. Integration: dep scoping ─────────────────────────── */
|
||||
|
||||
TEST(pagerank_after_dep_index) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "proj", "/tmp/proj");
|
||||
cbm_store_upsert_project(s, "proj.dep.lib", "/tmp/lib");
|
||||
int64_t a = add_node(s, "proj", "app_main");
|
||||
int64_t b = add_node(s, "proj.dep.lib", "lib_init");
|
||||
int64_t c = add_node(s, "proj.dep.lib", "lib_process");
|
||||
add_edge(s, "proj", a, b, "CALLS");
|
||||
add_edge(s, "proj.dep.lib", b, c, "CALLS");
|
||||
int rc = cbm_pagerank_compute_default(s, "proj");
|
||||
ASSERT_EQ(rc, 3);
|
||||
ASSERT_TRUE(get_pr(s, c) > 0.0);
|
||||
double total = get_pr(s, a) + get_pr(s, b) + get_pr(s, c);
|
||||
ASSERT_TRUE(fabs(total - 1.0) < 0.05);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite registration ──────────────────────────────────── */
|
||||
|
||||
SUITE(pagerank) {
|
||||
/* Core PageRank (14 tests) */
|
||||
RUN_TEST(pagerank_empty_graph);
|
||||
RUN_TEST(pagerank_single_node);
|
||||
RUN_TEST(pagerank_two_nodes_one_edge);
|
||||
RUN_TEST(pagerank_cycle);
|
||||
RUN_TEST(pagerank_star_topology);
|
||||
RUN_TEST(pagerank_edge_weights);
|
||||
RUN_TEST(pagerank_convergence);
|
||||
RUN_TEST(pagerank_sum_to_one);
|
||||
RUN_TEST(pagerank_stored_in_db);
|
||||
RUN_TEST(pagerank_recompute_replaces);
|
||||
RUN_TEST(pagerank_full_scope_includes_deps);
|
||||
RUN_TEST(pagerank_project_scope_excludes_deps);
|
||||
RUN_TEST(pagerank_dangling_nodes);
|
||||
RUN_TEST(pagerank_null_safety);
|
||||
/* Edge cases from igraph/NetworkX (13 tests) */
|
||||
RUN_TEST(pagerank_self_loop);
|
||||
RUN_TEST(pagerank_disconnected_components);
|
||||
RUN_TEST(pagerank_all_dangling_no_edges);
|
||||
RUN_TEST(pagerank_complete_graph);
|
||||
RUN_TEST(pagerank_multigraph_edges);
|
||||
RUN_TEST(pagerank_large_graph_stability);
|
||||
RUN_TEST(pagerank_zero_weight_edges);
|
||||
RUN_TEST(pagerank_custom_damping_high);
|
||||
RUN_TEST(pagerank_custom_damping_low);
|
||||
RUN_TEST(pagerank_max_iter_zero);
|
||||
RUN_TEST(pagerank_known_values);
|
||||
RUN_TEST(pagerank_known_values_asymmetric);
|
||||
RUN_TEST(pagerank_scope_deps_only);
|
||||
/* LinkRank (7 tests) */
|
||||
RUN_TEST(linkrank_computed_from_pagerank);
|
||||
RUN_TEST(linkrank_formula_correct);
|
||||
RUN_TEST(linkrank_calls_higher_than_usage);
|
||||
RUN_TEST(linkrank_stored_in_db);
|
||||
RUN_TEST(linkrank_self_loop_edge);
|
||||
RUN_TEST(linkrank_no_edges);
|
||||
RUN_TEST(linkrank_sum_equals_pagerank_sum);
|
||||
/* Integration (1 test) */
|
||||
RUN_TEST(pagerank_after_dep_index);
|
||||
}
|
||||
Reference in New Issue
Block a user