From 3fd21f8a23e1fb0cdcb10147b08416135dd5cf60 Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Sun, 22 Mar 2026 23:51:34 -0400 Subject: [PATCH] mcp: fix 3 dep search bugs found by binary dogfooding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/depindex/depindex.c | 11 ++++++++--- src/mcp/mcp.c | 38 +++++++++++++++++++++++++++++++------- src/store/store.c | 11 +++++++++-- tests/test_depindex.c | 7 +++++-- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/depindex/depindex.c b/src/depindex/depindex.c index 06a8780a8..4b09c42d7 100644 --- a/src/depindex/depindex.c +++ b/src/depindex/depindex.c @@ -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; } diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 25f764600..450604140 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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); } diff --git a/src/store/store.c b/src/store/store.c index ee940ea44..83836ce2d 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -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); diff --git a/tests/test_depindex.c b/tests/test_depindex.c index 39633f0f3..c421b5733 100644 --- a/tests/test_depindex.c +++ b/tests/test_depindex.c @@ -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(); }