Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ struct cbm_pipeline {

/* User-defined extension overrides (loaded once per run) */
cbm_userconfig_t *userconfig;

/* 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) ─────────────── */
Expand Down Expand Up @@ -176,6 +180,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 */
Expand Down Expand Up @@ -788,6 +795,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];
Expand Down Expand Up @@ -841,6 +864,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) {
Expand All @@ -867,6 +898,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) {
Expand Down
70 changes: 70 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,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");
Expand Down Expand Up @@ -5897,6 +5966,7 @@ SUITE(pipeline) {
RUN_TEST(store_bulk_persistence);
/* Integration: structure pass */
RUN_TEST(pipeline_structure_nodes);
RUN_TEST(pipeline_adr_survives_full_reindex);
RUN_TEST(pipeline_structure_edges);
RUN_TEST(pipeline_branch_root_structure);
RUN_TEST(pipeline_project_name_derived);
Expand Down
Loading