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(); }