From 5bb238e6a130b93568e17f16836446c0dc5541ee Mon Sep 17 00:00:00 2001 From: Mustapha Date: Fri, 31 Jul 2026 13:36:48 +0100 Subject: [PATCH 1/2] fix(pipeline): preserve ADRs during incremental reindex Signed-off-by: Mustapha --- src/pipeline/pipeline_incremental.c | 35 ++++++++++++-- tests/test_pipeline.c | 71 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 21740ba64..7b8398e83 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -645,25 +645,47 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p struct timespec t; cbm_clock_gettime(CLOCK_MONOTONIC, &t); + /* Preserve ADR content stored in project_summaries before replacing the DB. */ + char *saved_adr = NULL; + cbm_store_t *adr_store = cbm_store_open_path(db_path); + if (adr_store) { + cbm_adr_t existing = {0}; + if (cbm_store_adr_get(adr_store, project, &existing) == CBM_STORE_OK) { + if (existing.content) { + saved_adr = strdup(existing.content); + } + cbm_store_adr_free(&existing); + } + cbm_store_close(adr_store); + } + + int rc = CBM_STORE_ERR; + if ((cbm_unlink(db_path) != 0 && errno != ENOENT) || cbm_remove_db_sidecars(db_path) != 0) { cbm_log_error("incremental.err", "msg", "clear_staging_failed", "path", db_path); - return CBM_STORE_ERR; + goto cleanup; } int dump_rc = cbm_gbuf_dump_to_sqlite(gbuf, db_path); cbm_log_info("incremental.dump", "rc", itoa_buf(dump_rc), "elapsed_ms", itoa_buf((int)elapsed_ms(t))); if (dump_rc != 0) { - return dump_rc; + rc = dump_rc; + goto cleanup; } cbm_store_t *hash_store = cbm_store_open_path(db_path); if (!hash_store) { cbm_log_error("incremental.err", "msg", "open_staging_after_dump", "path", db_path); - return CBM_STORE_ERR; + goto cleanup; + } + /* #992: restore the captured ADR before persisting hashes, mirroring the + * full-reindex path (#516) -- the DB replacement above dropped it. */ + if (saved_adr && cbm_store_adr_store(hash_store, project, saved_adr) != CBM_STORE_OK) { + cbm_log_error("incremental.err", "msg", "adr_restore", "project", project); } - int rc = - persist_hashes(hash_store, project, files, file_count, mode_skipped, mode_skipped_count); + + rc = persist_hashes(hash_store, project, files, file_count, mode_skipped, mode_skipped_count); /* Coverage rows (#963): re-write the merged set into the rebuilt DB * (AFTER hashes, so the deleted-file prune sees the live file set). */ @@ -701,6 +723,9 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p } } cbm_store_close(hash_store); + +cleanup: + free(saved_adr); return rc; } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 9d72b4f58..59fcc0430 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -9,6 +9,7 @@ #include "test_framework.h" #include "test_helpers.h" #include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test) +#include "foundation/log.h" #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" #include "store/store.h" @@ -29,6 +30,13 @@ /* ── Helper: create temp test repo with known layout ───────────── */ static char g_tmpdir[256]; +static bool g_incremental_route_seen; + +static void capture_incremental_route(const char *line) { + if (strstr(line, "pipeline.route") && strstr(line, "incremental")) { + g_incremental_route_seen = true; + } +} /* Create: * /tmp/cbm_test_XXXXXX/ @@ -344,6 +352,68 @@ TEST(pipeline_adr_survives_full_reindex) { PASS(); } +TEST(pipeline_adr_survives_incremental_reindex) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_adr_incr_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("failed to create temp dir"); + } + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/test.db", tmp); + + char path[512]; + snprintf(path, sizeof(path), "%s/main.py", tmp); + FILE *f = fopen(path, "w"); + ASSERT_NOT_NULL(f); + fprintf(f, "def foo():\n return 1\n"); + fclose(f); + + cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p1); + ASSERT_EQ(cbm_pipeline_run(p1), 0); + const char *project = cbm_pipeline_project_name(p1); + char project_copy[256]; + snprintf(project_copy, sizeof(project_copy), "%s", project); + cbm_pipeline_free(p1); + + const char *adr_text = "# Decision\nIncremental reindex keeps ADR content."; + cbm_store_t *s1 = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s1); + ASSERT_EQ(cbm_store_adr_store(s1, project_copy, adr_text), CBM_STORE_OK); + cbm_store_close(s1); + + /* Change one existing file so cbm_pipeline_run routes through incremental + * reindex instead of the full delete/rebuild path. */ + f = fopen(path, "a"); + ASSERT_NOT_NULL(f); + fprintf(f, "\ndef bar():\n return 2\n"); + fclose(f); + + g_incremental_route_seen = false; + cbm_log_set_sink_ex(capture_incremental_route, CBM_LOG_SINK_REPLACE); + cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p2); + int pipeline_rc = cbm_pipeline_run(p2); + cbm_log_set_sink(NULL); + ASSERT_EQ(pipeline_rc, 0); + ASSERT_TRUE(g_incremental_route_seen); + cbm_pipeline_free(p2); + + cbm_store_t *s2 = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s2); + cbm_adr_t adr = {0}; + int rc = cbm_store_adr_get(s2, project_copy, &adr); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_NOT_NULL(adr.content); + ASSERT_STR_EQ(adr.content, adr_text); + cbm_store_adr_free(&adr); + cbm_store_close(s2); + + rm_rf(tmp); + PASS(); +} + TEST(pipeline_structure_edges) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); @@ -7772,6 +7842,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_structure_nodes); RUN_TEST(pipeline_committed_counts_match_persisted); RUN_TEST(pipeline_adr_survives_full_reindex); + RUN_TEST(pipeline_adr_survives_incremental_reindex); RUN_TEST(pipeline_structure_edges); RUN_TEST(pipeline_branch_root_structure); RUN_TEST(pipeline_project_name_derived); From 0cdd6fbd8ba1fddd1d0d94142dc9e758bf77fcd0 Mon Sep 17 00:00:00 2001 From: Mustapha Date: Fri, 31 Jul 2026 18:36:21 +0100 Subject: [PATCH 2/2] fix(pipeline): abort on ADR restore failure Signed-off-by: Mustapha --- src/pipeline/pipeline_incremental.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 7b8398e83..76ca0ff29 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -679,10 +679,12 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p cbm_log_error("incremental.err", "msg", "open_staging_after_dump", "path", db_path); goto cleanup; } + bool adr_restore_failed = false; /* #992: restore the captured ADR before persisting hashes, mirroring the * full-reindex path (#516) -- the DB replacement above dropped it. */ if (saved_adr && cbm_store_adr_store(hash_store, project, saved_adr) != CBM_STORE_OK) { cbm_log_error("incremental.err", "msg", "adr_restore", "project", project); + adr_restore_failed = true; } rc = persist_hashes(hash_store, project, files, file_count, mode_skipped, mode_skipped_count); @@ -723,6 +725,9 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p } } cbm_store_close(hash_store); + if (adr_restore_failed) { + rc = CBM_PIPELINE_ABORT_PRESERVE_DB; + } cleanup: free(saved_adr);