diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 8e370f7c..9d99a925 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -97,6 +97,10 @@ struct cbm_pipeline { /* Committed graph size at dump time (-1 = dump did not run). #334 gate axis. */ int committed_nodes; int committed_edges; + + /* ADR (project_summaries) captured before a full-reindex DB delete, so it + * can be restored after the rebuild. NULL when no ADR existed. Issue #516. */ + char *saved_adr; }; /* ── Global pkgmap (one active pipeline at a time) ─────────────── */ @@ -182,6 +186,9 @@ void cbm_pipeline_free(cbm_pipeline_t *p) { p->excluded_dirs = NULL; p->excluded_count = 0; free(p->branch_qn); + free(p->saved_adr); /* freed here too: error paths can exit before the + * restore in dump_and_persist_hashes runs. Issue #516. */ + p->saved_adr = NULL; cbm_git_context_free(&p->git_ctx); /* gbuf, store, registry freed during/after run */ /* Defensively free userconfig in case run() was never called or panicked */ @@ -810,6 +817,22 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file cbm_store_close(check_store); } cbm_log_info("pipeline.route", "path", "reindex", "action", "deleting old db"); + /* Capture any ADR before deleting the DB so the full-reindex rebuild can + * restore it (project_summaries is otherwise lost). Issue #516. */ + { + cbm_store_t *adr_store = cbm_store_open_path(db_path); + if (adr_store) { + cbm_adr_t existing; + if (cbm_store_adr_get(adr_store, p->project_name, &existing) == CBM_STORE_OK) { + if (existing.content) { + free(p->saved_adr); + p->saved_adr = strdup(existing.content); + } + cbm_store_adr_free(&existing); + } + cbm_store_close(adr_store); + } + } cbm_unlink(db_path); char wal[PL_WAL_BUF]; char shm[PL_WAL_BUF]; @@ -869,6 +892,14 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil cbm_store_t *hash_store = cbm_store_open_path(db_path); if (hash_store) { cbm_store_delete_file_hashes(hash_store, p->project_name); + + /* Restore the ADR captured before the dump. Surface a failed restore + * rather than silently dropping the ADR (the original #516 symptom). */ + if (p->saved_adr) { + if (cbm_store_adr_store(hash_store, p->project_name, p->saved_adr) != CBM_STORE_OK) { + cbm_log_error("pipeline.err", "phase", "adr_restore", "project", p->project_name); + } + } for (int i = 0; i < file_count; i++) { struct stat fst; if (stat(files[i].path, &fst) == 0) { @@ -895,6 +926,8 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil cbm_store_close(hash_store); cbm_log_info("pass.timing", "pass", "persist_hashes", "files", itoa_buf(file_count)); } + free(p->saved_adr); + p->saved_adr = NULL; /* Export persistent artifact if enabled */ if (p->persistence) { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 98b4204f..aca6c0d7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -273,6 +273,75 @@ TEST(pipeline_structure_nodes) { PASS(); } +/* Issue #516: an ADR stored via manage_adr (project_summaries) must survive a + * full re-index. A full re-index deletes the DB and rebuilds it from the graph + * buffer, which writes an empty project_summaries table; the fix captures the + * ADR before the delete and restores it after the rebuild. Reproduce-first: + * index, store an ADR, force a full re-index by adding files, assert the ADR + * is still present and unchanged. */ +TEST(pipeline_adr_survives_full_reindex) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_adr_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); + + /* Initial index with a single source file. */ + 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 pass\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); + + /* Store an ADR. */ + const char *adr_text = "# Decision\nWe chose X over Y."; + 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); + + /* Force a full re-index: add enough files to exceed the incremental + * threshold so the DB is deleted and rebuilt. */ + for (int i = 0; i < 4; i++) { + snprintf(path, sizeof(path), "%s/extra%d.py", tmp, i); + f = fopen(path, "w"); + ASSERT_NOT_NULL(f); + fprintf(f, "def g%d():\n return %d\n", i, i); + fclose(f); + } + + cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p2); + ASSERT_EQ(cbm_pipeline_run(p2), 0); + cbm_pipeline_free(p2); + + /* The ADR must still be present and unchanged. */ + 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"); @@ -6092,6 +6161,7 @@ SUITE(pipeline) { /* Integration: structure pass */ RUN_TEST(pipeline_structure_nodes); RUN_TEST(pipeline_committed_counts_match_persisted); + RUN_TEST(pipeline_adr_survives_full_reindex); RUN_TEST(pipeline_structure_edges); RUN_TEST(pipeline_branch_root_structure); RUN_TEST(pipeline_project_name_derived);