mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
feat(store): list symbol scope qns
Add a bounded store helper that expands imported symbol QNs to exact and dotted-member QNs in project scope, preserving input order and reporting truncation. Use it in the scoped cross-LSP diagnostic so persisted Python defs are discovered from the imported Logger scope instead of a hand-built member list, while proving OtherLogger remains excluded. Validation: make -f Makefile.cbm -j16 build/c/test-runner; CBM_ONLY_SUITE=store_nodes; CBM_ONLY_SUITE=pipeline; focused store/pipeline tests; scripts/check-source-safety.sh; git diff --check. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -405,6 +405,18 @@ static int store_append_text(char ***items, int *count, int *cap, const char *te
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
static bool store_text_array_contains(char *const *items, int count, const char *text) {
|
||||
if (!items || !text) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (items[i] && strcmp(items[i], text) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int store_grow_array(cbm_store_t *s, void **items, int *cap, size_t item_size,
|
||||
const char *op, bool zero_new) {
|
||||
if (!items || !cap || item_size == 0 || *cap <= 0 || *cap > INT_MAX / ST_GROWTH) {
|
||||
@@ -2070,6 +2082,140 @@ int cbm_store_find_nodes_by_qns(cbm_store_t *s, const char *project, const char
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
int cbm_store_list_symbol_scope_qns_by_qns(cbm_store_t *s, const char *project,
|
||||
const char **qns, int qn_count, int max_qns,
|
||||
char ***out, int *count, bool *out_truncated) {
|
||||
if (!out || !count) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
*out = NULL;
|
||||
*count = 0;
|
||||
if (out_truncated) {
|
||||
*out_truncated = false;
|
||||
}
|
||||
if (!s || !s->db || !project || max_qns <= 0) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
if (qn_count <= 0) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
if (!qns) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
int cap = max_qns < ST_INIT_CAP_8 ? max_qns : ST_INIT_CAP_8;
|
||||
char **items = malloc((size_t)cap * sizeof(*items));
|
||||
if (!items) {
|
||||
store_set_error(s, "list_symbol_scope_qns out of memory");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
int offset = 0;
|
||||
bool truncated = false;
|
||||
const int sqlite_bind_limit = sqlite3_limit(s->db, SQLITE_LIMIT_VARIABLE_NUMBER, -1);
|
||||
const int qn_bind_limit =
|
||||
sqlite_bind_limit > SKIP_ONE ? (sqlite_bind_limit - SKIP_ONE) / PAIR_LEN : SKIP_ONE;
|
||||
static const char prefix[] = "WITH q(ord, qn) AS (VALUES ";
|
||||
static const char suffix[] =
|
||||
"), candidates AS ("
|
||||
" SELECT q.ord,"
|
||||
" CASE WHEN n.qualified_name = q.qn THEN 0 ELSE 1 END AS member_rank,"
|
||||
" n.qualified_name"
|
||||
" FROM q JOIN nodes n ON n.project = ?"
|
||||
" AND (n.qualified_name = q.qn"
|
||||
/* '/' is the byte after '.', so this is a bounded prefix scan for qn + '.'. */
|
||||
" OR (n.qualified_name >= q.qn || '.' AND n.qualified_name < q.qn || '/'))"
|
||||
") SELECT qualified_name FROM candidates ORDER BY ord, member_rank, qualified_name;";
|
||||
|
||||
while (offset < qn_count && !truncated) {
|
||||
char sql[ST_SQL_BUF];
|
||||
int pos = snprintf(sql, sizeof(sql), "%s", prefix);
|
||||
if (pos < 0 || pos >= (int)sizeof(sql)) {
|
||||
store_free_text_array(items, n);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
int chunk_end = offset;
|
||||
int bind_count = 0;
|
||||
for (; chunk_end < qn_count && bind_count < qn_bind_limit; chunk_end++) {
|
||||
if (!qns[chunk_end]) {
|
||||
continue;
|
||||
}
|
||||
static const char row_sql[] = "(?,?)";
|
||||
int extra = (bind_count > 0 ? 1 : 0) + (int)SLEN(row_sql) + (int)SLEN(suffix);
|
||||
if (pos + extra + ST_IN_CLAUSE_MARGIN >= (int)sizeof(sql)) {
|
||||
break;
|
||||
}
|
||||
if (bind_count > 0) {
|
||||
sql[pos++] = ',';
|
||||
}
|
||||
memcpy(sql + pos, row_sql, SLEN(row_sql));
|
||||
pos += (int)SLEN(row_sql);
|
||||
bind_count++;
|
||||
}
|
||||
if (bind_count == 0) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
if (pos + (int)SLEN(suffix) >= (int)sizeof(sql)) {
|
||||
store_free_text_array(items, n);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
memcpy(sql + pos, suffix, SLEN(suffix) + 1);
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "list_symbol_scope_qns prepare");
|
||||
store_free_text_array(items, n);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
int bind_col = SKIP_ONE;
|
||||
for (int i = offset; i < chunk_end; i++) {
|
||||
if (!qns[i]) {
|
||||
continue;
|
||||
}
|
||||
sqlite3_bind_int(stmt, bind_col++, i);
|
||||
bind_text(stmt, bind_col++, qns[i]);
|
||||
}
|
||||
bind_text(stmt, bind_col, project);
|
||||
|
||||
int step_rc = SQLITE_OK;
|
||||
while ((step_rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
const char *candidate = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (!candidate || store_text_array_contains(items, n, candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (n >= max_qns) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
int rc = store_append_text(&items, &n, &cap, candidate);
|
||||
if (rc != CBM_STORE_OK) {
|
||||
sqlite3_finalize(stmt);
|
||||
store_free_text_array(items, n);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
if (step_rc != SQLITE_DONE && !truncated) {
|
||||
store_set_error_sqlite(s, "list_symbol_scope_qns step");
|
||||
sqlite3_finalize(stmt);
|
||||
store_free_text_array(items, n);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
offset = chunk_end;
|
||||
}
|
||||
|
||||
*out = items;
|
||||
*count = n;
|
||||
if (out_truncated) {
|
||||
*out_truncated = truncated;
|
||||
}
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
static int collect_nodes_from_stmt(cbm_store_t *s, sqlite3_stmt *stmt, const char *op,
|
||||
cbm_node_t **out, int *count) {
|
||||
if (!out || !count) {
|
||||
|
||||
@@ -532,6 +532,15 @@ int cbm_store_find_node_ids_by_qns(cbm_store_t *s, const char *project, const ch
|
||||
int cbm_store_find_nodes_by_qns(cbm_store_t *s, const char *project, const char **qns,
|
||||
int qn_count, cbm_node_t **out, int *count);
|
||||
|
||||
/* Candidate scope expansion for bounded resolver context.
|
||||
* Returns exact QNs plus member QNs below each input QN ("Type.member"),
|
||||
* preserving first input order and skipping duplicates/missing/null inputs.
|
||||
* max_qns must be positive; out_truncated is optional. Caller frees each
|
||||
* returned string and the array. */
|
||||
int cbm_store_list_symbol_scope_qns_by_qns(cbm_store_t *s, const char *project,
|
||||
const char **qns, int qn_count, int max_qns,
|
||||
char ***out, int *count, bool *out_truncated);
|
||||
|
||||
/* Count nodes in project. Returns count or CBM_STORE_ERR. */
|
||||
int cbm_store_count_nodes(cbm_store_t *s, const char *project);
|
||||
int cbm_store_count_nodes_scoped(cbm_store_t *s, const char *project, const char *path);
|
||||
|
||||
+43
-22
@@ -2136,6 +2136,18 @@ static bool pipeline_resolved_call_contains(const CBMResolvedCallArray *arr,
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool pipeline_text_array_contains(char *const *items, int count, const char *needle) {
|
||||
if (!items || !needle) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (items[i] && strcmp(items[i], needle) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void pipeline_restore_workers_env(bool had_workers, const char *saved_workers) {
|
||||
if (had_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
@@ -12142,7 +12154,7 @@ TEST(incremental_overlay_python_receiver_type_gap_uses_full_reindex) {
|
||||
}
|
||||
|
||||
TEST(pipeline_persisted_python_defs_feed_scoped_cross_lsp) {
|
||||
enum { PIPELINE_PERSISTED_DEF_CAP = 3 };
|
||||
enum { PIPELINE_PERSISTED_SCOPE_CAP = CBM_SZ_8 };
|
||||
if (setup_incremental_repo() != 0) {
|
||||
FAIL("setup failed");
|
||||
}
|
||||
@@ -12209,30 +12221,38 @@ TEST(pipeline_persisted_python_defs_feed_scoped_cross_lsp) {
|
||||
cbm_store_t *store = cbm_store_open_path(g_incr_dbpath);
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
char *provider_qns[PIPELINE_PERSISTED_DEF_CAP] = {
|
||||
cbm_pipeline_fqn_compute(project, "provider.py", "Logger"),
|
||||
cbm_pipeline_fqn_compute(project, "provider.py", "Logger.log"),
|
||||
cbm_pipeline_fqn_compute(project, "provider.py", "OtherLogger.log"),
|
||||
};
|
||||
for (int i = 0; i < PIPELINE_PERSISTED_DEF_CAP; i++) {
|
||||
ASSERT_NOT_NULL(provider_qns[i]);
|
||||
}
|
||||
const char *provider_qn_view[PIPELINE_PERSISTED_DEF_CAP] = {
|
||||
provider_qns[0],
|
||||
provider_qns[1],
|
||||
provider_qns[2],
|
||||
};
|
||||
char *import_qn = cbm_pipeline_fqn_compute(project, "provider.py", "Logger");
|
||||
char *provider_log_qn = cbm_pipeline_fqn_compute(project, "provider.py", "Logger.log");
|
||||
char *other_log_qn = cbm_pipeline_fqn_compute(project, "provider.py", "OtherLogger.log");
|
||||
ASSERT_NOT_NULL(import_qn);
|
||||
ASSERT_NOT_NULL(provider_log_qn);
|
||||
ASSERT_NOT_NULL(other_log_qn);
|
||||
|
||||
const char *scope_inputs[] = {import_qn};
|
||||
char **candidate_qns = NULL;
|
||||
int candidate_qn_count = 0;
|
||||
bool candidate_truncated = true;
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(
|
||||
store, project, scope_inputs, CBM_ALLOC_ONE, PIPELINE_PERSISTED_SCOPE_CAP,
|
||||
&candidate_qns, &candidate_qn_count, &candidate_truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_FALSE(candidate_truncated);
|
||||
ASSERT_EQ(candidate_qn_count, PAIR_LEN);
|
||||
ASSERT_TRUE(pipeline_text_array_contains(candidate_qns, candidate_qn_count, import_qn));
|
||||
ASSERT_TRUE(pipeline_text_array_contains(candidate_qns, candidate_qn_count, provider_log_qn));
|
||||
ASSERT_FALSE(pipeline_text_array_contains(candidate_qns, candidate_qn_count, other_log_qn));
|
||||
|
||||
cbm_node_t *provider_nodes = NULL;
|
||||
int provider_node_count = 0;
|
||||
ASSERT_EQ(cbm_store_find_nodes_by_qns(store, project, provider_qn_view,
|
||||
PIPELINE_PERSISTED_DEF_CAP, &provider_nodes,
|
||||
ASSERT_EQ(cbm_store_find_nodes_by_qns(store, project, (const char **)candidate_qns,
|
||||
candidate_qn_count, &provider_nodes,
|
||||
&provider_node_count),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(provider_node_count, PIPELINE_PERSISTED_DEF_CAP);
|
||||
ASSERT_EQ(provider_node_count, PAIR_LEN);
|
||||
|
||||
CBMArena persisted_arena;
|
||||
cbm_arena_init(&persisted_arena);
|
||||
CBMLSPDef persisted_defs[PIPELINE_PERSISTED_DEF_CAP];
|
||||
CBMLSPDef persisted_defs[PIPELINE_PERSISTED_SCOPE_CAP];
|
||||
memset(persisted_defs, 0, sizeof(persisted_defs));
|
||||
int persisted_count = 0;
|
||||
for (int i = 0; i < provider_node_count; i++) {
|
||||
@@ -12253,9 +12273,7 @@ TEST(pipeline_persisted_python_defs_feed_scoped_cross_lsp) {
|
||||
(size_t)persisted_count * sizeof(*all_defs));
|
||||
|
||||
char *module_qn = cbm_pipeline_fqn_module(project, "service.py");
|
||||
char *import_qn = cbm_pipeline_fqn_compute(project, "provider.py", "Logger");
|
||||
ASSERT_NOT_NULL(module_qn);
|
||||
ASSERT_NOT_NULL(import_qn);
|
||||
const char *imp_names[] = {"Logger"};
|
||||
const char *imp_qns[] = {import_qn};
|
||||
CBMArena out_arena;
|
||||
@@ -12269,11 +12287,14 @@ TEST(pipeline_persisted_python_defs_feed_scoped_cross_lsp) {
|
||||
|
||||
cbm_arena_destroy(&out_arena);
|
||||
free(import_qn);
|
||||
free(provider_log_qn);
|
||||
free(other_log_qn);
|
||||
free(module_qn);
|
||||
free(all_defs);
|
||||
for (int i = 0; i < PIPELINE_PERSISTED_DEF_CAP; i++) {
|
||||
free(provider_qns[i]);
|
||||
for (int i = 0; i < candidate_qn_count; i++) {
|
||||
free(candidate_qns[i]);
|
||||
}
|
||||
free(candidate_qns);
|
||||
cbm_arena_destroy(&persisted_arena);
|
||||
free(own_defs);
|
||||
free(changed_modules[0]);
|
||||
|
||||
@@ -5190,6 +5190,93 @@ TEST(store_find_nodes_by_qns_returns_full_rows_in_input_order) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_list_symbol_scope_qns_by_qns_expands_exact_and_members) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_TRUE(cbm_store_upsert_project(s, "test", "/tmp/test") == CBM_STORE_OK);
|
||||
ASSERT_TRUE(cbm_store_upsert_project(s, "other", "/tmp/other") == CBM_STORE_OK);
|
||||
|
||||
cbm_node_t cls = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "Logger",
|
||||
.qualified_name = "test.provider.Logger"};
|
||||
cbm_node_t debug = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "debug",
|
||||
.qualified_name = "test.provider.Logger.debug"};
|
||||
cbm_node_t log = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "log",
|
||||
.qualified_name = "test.provider.Logger.log"};
|
||||
cbm_node_t sibling = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "log",
|
||||
.qualified_name = "test.provider.LoggerExtra.log"};
|
||||
cbm_node_t unrelated = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "log",
|
||||
.qualified_name = "test.provider.OtherLogger.log"};
|
||||
cbm_node_t other_project = {.project = "other",
|
||||
.label = "Method",
|
||||
.name = "trace",
|
||||
.qualified_name = "test.provider.Logger.trace"};
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &cls) > 0);
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &debug) > 0);
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &log) > 0);
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &sibling) > 0);
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &unrelated) > 0);
|
||||
ASSERT_TRUE(cbm_store_upsert_node(s, &other_project) > 0);
|
||||
|
||||
const char *scopes[] = {"test.provider.Logger", "missing.Q", NULL, "test.provider.Logger"};
|
||||
char **qns = NULL;
|
||||
int count = 0;
|
||||
bool truncated = true;
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(
|
||||
s, "test", scopes, (int)(sizeof(scopes) / sizeof(scopes[0])), CBM_SZ_16, &qns,
|
||||
&count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_FALSE(truncated);
|
||||
ASSERT_EQ(count, 3);
|
||||
ASSERT_STR_EQ(qns[0], "test.provider.Logger");
|
||||
ASSERT_STR_EQ(qns[1], "test.provider.Logger.debug");
|
||||
ASSERT_STR_EQ(qns[2], "test.provider.Logger.log");
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(qns[i]);
|
||||
}
|
||||
free(qns);
|
||||
|
||||
qns = NULL;
|
||||
count = 0;
|
||||
truncated = false;
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(
|
||||
s, "test", scopes, (int)(sizeof(scopes) / sizeof(scopes[0])), PAIR_LEN, &qns,
|
||||
&count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_TRUE(truncated);
|
||||
ASSERT_EQ(count, PAIR_LEN);
|
||||
ASSERT_STR_EQ(qns[0], "test.provider.Logger");
|
||||
ASSERT_STR_EQ(qns[1], "test.provider.Logger.debug");
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(qns[i]);
|
||||
}
|
||||
free(qns);
|
||||
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(
|
||||
s, "test", scopes, (int)(sizeof(scopes) / sizeof(scopes[0])), 0, &qns, &count,
|
||||
NULL),
|
||||
CBM_STORE_ERR);
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(s, "test", NULL, 1, CBM_SZ_16, &qns,
|
||||
&count, NULL),
|
||||
CBM_STORE_ERR);
|
||||
ASSERT_EQ(cbm_store_list_symbol_scope_qns_by_qns(s, "test", NULL, 0, CBM_SZ_16, &qns,
|
||||
&count, NULL),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Integrity check tests ──────────────────────────────────────── */
|
||||
|
||||
TEST(store_integrity_clean) {
|
||||
@@ -5927,6 +6014,7 @@ SUITE(store_nodes) {
|
||||
RUN_TEST(store_pragma_settings);
|
||||
RUN_TEST(store_find_node_ids_by_qns);
|
||||
RUN_TEST(store_find_nodes_by_qns_returns_full_rows_in_input_order);
|
||||
RUN_TEST(store_list_symbol_scope_qns_by_qns_expands_exact_and_members);
|
||||
RUN_TEST(store_node_null_project);
|
||||
RUN_TEST(store_node_null_qn);
|
||||
RUN_TEST(store_node_empty_strings);
|
||||
|
||||
Reference in New Issue
Block a user