From 2479724a35d252786b8dbbeb9f1a74d86c569620 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Tue, 29 Sep 2026 09:25:46 +0000 Subject: [PATCH 1/5] fix(pipeline): refresh the projects row's root_path on the delta route run_closure_delta clones the previous generation's db file byte for byte and patches only nodes, edges, hashes and lsp surfaces, so a reindex that keeps the same project name but a different repo_path leaves root_path and indexed_at pinned to whichever directory the last full rebuild ran from. Every later root_path-keyed read then resolves files against a directory this run never touched. cbm_store_upsert_project already exists for exactly this purpose and is called on the full-rebuild path via cbm_gbuf_dump_to_sqlite; the delta route reaches cbm_pipeline_publish_staged directly and never calls it. Call it once on the staging store before publishing. Fixes #2276 Signed-off-by: Amir Fathi --- src/pipeline/pipeline_incremental.c | 11 +++++ tests/test_pipeline.c | 68 +++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 154518658..1574c31b9 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -2319,6 +2319,17 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char cbm_store_count_edges(staging, project)); cbm_log_info("delta.committed_counts", "elapsed_ms", itoa_buf((int)elapsed_ms(t))); + /* The staging file started as a byte-for-byte clone of the previous + * generation (cbm_delta_stage_clone above), so its `projects` row still + * carries whichever root_path/indexed_at the LAST full rebuild wrote. + * Every other write in this function patches nodes/edges/hashes/surfaces + * for the CURRENT run's repo_path, so the project row must be refreshed + * to match, or a later root_path-keyed read (get_code_snippet and + * friends) resolves files against a directory this run never touched. */ + if (cbm_store_upsert_project(staging, project, cbm_pipeline_repo_path(p)) != CBM_STORE_OK) { + goto out; + } + { int index_mode = cbm_pipeline_get_mode(p); cbm_pipeline_generation_t generation = { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 568740f17..6ccde01e3 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3369,6 +3369,73 @@ TEST(pipeline_closure_repair_removed_def_drops_dependent_edge) { PASS(); } +/* #2276: the closure/delta route clones the previous generation's db file + * byte-for-byte and patches only nodes/edges/hashes/surfaces, so a reindex + * that keeps the same project name but a DIFFERENT repo_path (an artifact + * moved or reused from another checkout) leaves the `projects` row's + * root_path/indexed_at pinned to whichever directory the last FULL rebuild + * ran from, so every later root_path-keyed read then resolves files against + * a directory this run never touched. */ +TEST(pipeline_incremental_root_path_refreshed_across_directories) { + char dir_a[256]; + char dir_b[256]; + snprintf(dir_a, sizeof(dir_a), "/tmp/cbm_rootpath_a_XXXXXX"); + snprintf(dir_b, sizeof(dir_b), "/tmp/cbm_rootpath_b_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(dir_a)); + ASSERT_NOT_NULL(cbm_mkdtemp(dir_b)); + closure_probe_repo(dir_a); + closure_probe_repo(dir_b); + /* Body edit only, same exported names: the shape the closure route + * repairs without falling back to a full rebuild (mirrors + * pipeline_closure_repair_body_edit_converges_with_fresh_full above). */ + write_temp_file(dir_b, "lib.ts", + "export function closureProbeHelper(x: number): string {\n" + " const doubled = x + x;\n" + " return String(doubled);\n" + "}\n"); + + char db[512]; + snprintf(db, sizeof(db), "%s/rootpath.db", dir_a); + static const char *project = "rootpath-probe"; + + cbm_pipeline_t *first = cbm_pipeline_new(dir_a, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(first); + ASSERT_TRUE(cbm_pipeline_set_project_name(first, project)); + ASSERT_EQ(cbm_pipeline_run(first), 0); + cbm_pipeline_free(first); + + /* Reindex the SAME project name from dir_b: a valid existing generation + * exists, so this routes through the incremental path, not a first-time + * full build. */ + cbm_pipeline_t *second = cbm_pipeline_new(dir_b, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(second); + ASSERT_TRUE(cbm_pipeline_set_project_name(second, project)); + ASSERT_EQ(cbm_pipeline_run(second), 0); + cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); + cbm_pipeline_free(second); + /* The bug is specific to the closure/delta route; confirm we actually + * exercised it rather than a silent fall-back to a full rebuild, which + * would pass even with the fix reverted. */ + ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + + char canon_b[PATH_MAX]; + ASSERT_NOT_NULL(realpath(dir_b, canon_b)); + + cbm_project_t info = {0}; + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + ASSERT_EQ(cbm_store_get_project(store, project, &info), CBM_STORE_OK); + ASSERT_STR_EQ(info.root_path, canon_b); + free((char *)info.name); + free((char *)info.indexed_at); + free((char *)info.root_path); + cbm_store_close(store); + + th_rmtree(dir_a); + th_rmtree(dir_b); + PASS(); +} + TEST(pipeline_closure_repair_added_name_declines_to_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_added_XXXXXX"); @@ -16182,6 +16249,7 @@ SUITE(pipeline_semantic_manifest_repro) { RUN_TEST(pipeline_dbt_jinja_lineage); RUN_TEST(pipeline_parallel_manifest_is_byte_stable_above_threshold); RUN_TEST(pipeline_closure_repair_body_edit_converges_with_fresh_full); + RUN_TEST(pipeline_incremental_root_path_refreshed_across_directories); RUN_TEST(pipeline_closure_repair_removed_def_drops_dependent_edge); RUN_TEST(pipeline_closure_repair_added_name_declines_to_full); RUN_TEST(pipeline_closure_repair_new_file_declines_to_full); From 4f7f8acca2aba292bd3f51a73dfa7288e11880c4 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Wed, 30 Sep 2026 05:04:25 +0000 Subject: [PATCH 2/5] fix(test): compare root_path against dir_b, not a realpath()'d form cbm_pipeline_repo_path()/cbm_store_upsert_project() never call realpath(); root_path is stored verbatim on both the full-rebuild and delta routes. The test's realpath(dir_b) comparison only held because /tmp isn't a symlink on Linux; it resolves to /private/tmp on macOS, where the assertion failed. Signed-off-by: Amir Fathi --- tests/test_pipeline.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 6ccde01e3..2ac38d2d7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3418,14 +3418,17 @@ TEST(pipeline_incremental_root_path_refreshed_across_directories) { * would pass even with the fix reverted. */ ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); - char canon_b[PATH_MAX]; - ASSERT_NOT_NULL(realpath(dir_b, canon_b)); - + /* cbm_pipeline_repo_path()/cbm_store_upsert_project() never call + * realpath(): the stored root_path is whatever was passed to + * cbm_pipeline_new(), verbatim, on both the full-rebuild path and this + * one. Compare against dir_b directly, not a canonicalized form, or + * this assertion only holds on a platform where /tmp is not itself a + * symlink (it resolves to /private/tmp on macOS). */ cbm_project_t info = {0}; cbm_store_t *store = cbm_store_open_path(db); ASSERT_NOT_NULL(store); ASSERT_EQ(cbm_store_get_project(store, project, &info), CBM_STORE_OK); - ASSERT_STR_EQ(info.root_path, canon_b); + ASSERT_STR_EQ(info.root_path, dir_b); free((char *)info.name); free((char *)info.indexed_at); free((char *)info.root_path); From 19374f2d340df14c0273b1381c1ad2390b0157d9 Mon Sep 17 00:00:00 2001 From: harshitaajoshi Date: Mon, 28 Sep 2026 15:02:38 -0500 Subject: [PATCH 3/5] fix(mcp): apply search_graph label filter inside the BM25 candidate window (#2386) Signed-off-by: harshitaajoshi --- src/mcp/mcp.c | 68 ++++++++++++++++++--- tests/test_mcp.c | 150 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 7 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 03741e23a..60cef5334 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -3969,6 +3969,7 @@ enum { BM25_BIND_EXACT = 8, BM25_BIND_KIND = 9, BM25_BIND_NS = 10, + BM25_BIND_LABEL_WEIGHT = 11, BM25_SQL_AUTO_LEN = -1, /* Inner FTS5 candidate cap. SQLite can early-terminate a plain FTS5 query * (no JOIN/WHERE on outer table) of the form: @@ -3977,6 +3978,11 @@ enum { * and then joining/filtering/re-ranking those, we bound all work to O(N) where * N = BM25_INNER_LIMIT rather than the full match set size. */ BM25_INNER_LIMIT = 2000, + /* Longest label spliced into the MATCH pre-filter; real labels are short + * identifiers, so anything longer simply skips the pre-filter. */ + BM25_LABEL_MAX = 64, + /* Room for "() AND label : \"