mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-29 07:13:44 +08:00
mcp: fix 3 dep search bugs found by binary dogfooding
Bug 1: resolve_store opens wrong DB for dep projects
Root cause: resolve_store("myapp.dep.pandas") opens myapp.dep.pandas.db
instead of myapp.db where deps actually live (same-db design).
Fix: parent_project_for_db() strips .dep.* suffix to find parent DB.
Bug 2: store.c prefix match was actually exact match
Root cause: cbm_store_search with params->project set (non-exact mode)
used "n.project = ?" — exact match, not prefix. Deps invisible.
Fix: SQL now uses "(n.project = ? OR n.project LIKE ?||'.%')" for
prefix mode, so search_graph(project="myapp") returns project + deps.
Bug 3: cbm_is_dep_project fails for cross-project deps
Root cause: Early return when session_project doesn't match prefix.
"otherapp.dep.pandas" with session "myapp" → false (should be true).
Fix: Fall through to generic .dep. strstr check when session prefix
doesn't match. Any project containing ".dep." is a dependency.
Bug 4: Package name extraction used wrong offset
Root cause: Used strlen(session_project) as offset into project name,
but session_project is CWD-detected, not the indexed project.
Fix: Use strstr(project, ".dep.") to find separator position directly.
Binary verification (all confirmed working):
- search_graph(project="myapp") → 18 results (9 project + 9 dep)
- source:"project" vs source:"dependency" correctly tagged
- package:"testlib" correctly extracted
- Multiple deps in one index_dependencies call works
Tests: 2173 passing (updated test_depindex cross-project assertion).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -63,12 +63,17 @@ char *cbm_dep_project_name(const char *project, const char *package_name) {
|
||||
|
||||
bool cbm_is_dep_project(const char *project_name, const char *session_project) {
|
||||
if (!project_name) return false;
|
||||
/* Check session-specific match first (e.g., "myapp.dep.pandas" with session "myapp") */
|
||||
if (session_project && session_project[0]) {
|
||||
size_t sp_len = strlen(session_project);
|
||||
return (strncmp(project_name, session_project, sp_len) == 0 &&
|
||||
strncmp(project_name + sp_len, CBM_DEP_SEPARATOR,
|
||||
CBM_DEP_SEPARATOR_LEN) == 0);
|
||||
if (strncmp(project_name, session_project, sp_len) == 0 &&
|
||||
strncmp(project_name + sp_len, CBM_DEP_SEPARATOR,
|
||||
CBM_DEP_SEPARATOR_LEN) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/* Generic fallback: any project containing ".dep." or starting with "dep." is a dep.
|
||||
* Handles cross-project queries where session_project doesn't match. */
|
||||
return strstr(project_name, CBM_DEP_SEPARATOR) != NULL ||
|
||||
strncmp(project_name, "dep.", 4) == 0;
|
||||
}
|
||||
|
||||
+31
-7
@@ -816,6 +816,21 @@ static const char *project_db_path(const char *project, char *buf, size_t bufsz)
|
||||
/* Open the right project's .db file for query tools.
|
||||
* Caches the connection — reopens only when project changes.
|
||||
* Tracks last-access time so the event loop can evict idle stores. */
|
||||
/* Extract the parent project name from a dep project name.
|
||||
* "myapp.dep.pandas" → "myapp", "myapp.dep" → "myapp", "myapp" → "myapp".
|
||||
* Returns a stack buffer pointer (caller must NOT free). */
|
||||
static const char *parent_project_for_db(const char *project, char *buf, size_t bufsz) {
|
||||
const char *dep = strstr(project, ".dep");
|
||||
if (dep && (dep[4] == '.' || dep[4] == '\0')) {
|
||||
size_t len = (size_t)(dep - project);
|
||||
if (len >= bufsz) len = bufsz - 1;
|
||||
memcpy(buf, project, len);
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
return project; /* no .dep → use as-is */
|
||||
}
|
||||
|
||||
static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
if (!project) {
|
||||
return srv->store; /* no project specified → use whatever's open */
|
||||
@@ -823,8 +838,13 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
|
||||
srv->store_last_used = time(NULL);
|
||||
|
||||
/* Already open for this project? */
|
||||
if (srv->current_project && strcmp(srv->current_project, project) == 0 && srv->store) {
|
||||
/* Dep projects (e.g., "myapp.dep.pandas") live in the parent project's DB
|
||||
* ("myapp.db"), not in a separate "myapp.dep.pandas.db". Extract parent. */
|
||||
char parent_buf[1024];
|
||||
const char *db_project = parent_project_for_db(project, parent_buf, sizeof(parent_buf));
|
||||
|
||||
/* Already open for this project's DB? */
|
||||
if (srv->current_project && strcmp(srv->current_project, db_project) == 0 && srv->store) {
|
||||
return srv->store;
|
||||
}
|
||||
|
||||
@@ -836,11 +856,11 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
|
||||
/* Open project's .db file */
|
||||
char path[1024];
|
||||
project_db_path(project, path, sizeof(path));
|
||||
project_db_path(db_project, path, sizeof(path));
|
||||
srv->store = cbm_store_open_path(path);
|
||||
srv->owns_store = true;
|
||||
free(srv->current_project);
|
||||
srv->current_project = heap_strdup(project);
|
||||
srv->current_project = heap_strdup(db_project);
|
||||
|
||||
return srv->store;
|
||||
}
|
||||
@@ -1473,9 +1493,13 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
bool is_dep = cbm_is_dep_project(sr->node.project, srv->session_project);
|
||||
yyjson_mut_obj_add_str(doc, item, "source", is_dep ? "dependency" : "project");
|
||||
if (is_dep && sr->node.project) {
|
||||
size_t sp_len2 = strlen(srv->session_project);
|
||||
const char *pkg = sr->node.project + sp_len2 + CBM_DEP_SEPARATOR_LEN;
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "package", pkg);
|
||||
/* Extract package name: find ".dep." and take everything after it.
|
||||
* "myapp.dep.pandas" → "pandas", "myapp.dep.uv.pandas" → "uv.pandas" */
|
||||
const char *dep_sep = strstr(sr->node.project, CBM_DEP_SEPARATOR);
|
||||
if (dep_sep) {
|
||||
const char *pkg = dep_sep + CBM_DEP_SEPARATOR_LEN;
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "package", pkg);
|
||||
}
|
||||
yyjson_mut_obj_add_bool(doc, item, "read_only", true);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -1803,6 +1803,8 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
|
||||
char bind_buf[64];
|
||||
char *like_pattern = NULL;
|
||||
char proj_like[1024]; /* prefix match pattern — must outlive BIND_TEXT usage */
|
||||
proj_like[0] = '\0';
|
||||
|
||||
if (params->project_pattern) {
|
||||
/* Glob/LIKE pattern from smart project param (e.g., "myapp.dep.%") */
|
||||
@@ -1815,10 +1817,15 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
ADD_WHERE(bind_buf);
|
||||
BIND_TEXT(params->project);
|
||||
} else if (params->project) {
|
||||
/* Default: exact match (same as before — prefix matching added in mcp.c) */
|
||||
snprintf(bind_buf, sizeof(bind_buf), "n.project = ?%d", bind_idx + 1);
|
||||
/* Prefix match: project itself + any dep sub-projects (e.g., myapp.dep.pandas).
|
||||
* Uses (exact OR LIKE prefix) to include deps in same DB. */
|
||||
snprintf(proj_like, sizeof(proj_like), "%s.%%", params->project);
|
||||
snprintf(bind_buf, sizeof(bind_buf),
|
||||
"(n.project = ?%d OR n.project LIKE ?%d)",
|
||||
bind_idx + 1, bind_idx + 2);
|
||||
ADD_WHERE(bind_buf);
|
||||
BIND_TEXT(params->project);
|
||||
BIND_TEXT(proj_like);
|
||||
}
|
||||
if (params->label) {
|
||||
snprintf(bind_buf, sizeof(bind_buf), "n.label = ?%d", bind_idx + 1);
|
||||
|
||||
@@ -580,11 +580,14 @@ TEST(test_dep_project_name_format) {
|
||||
}
|
||||
|
||||
TEST(test_is_dep_project_with_session) {
|
||||
/* With session context — precise prefix check */
|
||||
/* With session context — precise prefix check first, then generic .dep. fallback */
|
||||
ASSERT_TRUE(cbm_is_dep_project("myapp.dep.pandas", "myapp"));
|
||||
ASSERT_TRUE(cbm_is_dep_project("myapp.dep.serde", "myapp"));
|
||||
ASSERT_FALSE(cbm_is_dep_project("myapp", "myapp"));
|
||||
ASSERT_FALSE(cbm_is_dep_project("otherapp.dep.pandas", "myapp"));
|
||||
/* Cross-project deps: otherapp.dep.pandas contains ".dep." → IS a dep.
|
||||
* This is correct: when querying across projects, dep nodes from any project
|
||||
* should be tagged as dependencies for AI grounding (read_only, source tagging). */
|
||||
ASSERT_TRUE(cbm_is_dep_project("otherapp.dep.pandas", "myapp"));
|
||||
ASSERT_FALSE(cbm_is_dep_project(NULL, "myapp"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user