perf(pipeline): prove an unchanged re-index on the live generation

Every index_repository call copied the whole existing database into a
stage (sqlite3 backup) before routing, and the exact no-op then sealed
that copy and renamed it back over the live file. On the Linux kernel
index the copy alone was a 19 GB write per unchanged re-index.

The copy is now deferred until routing needs it. The exact no-op is
decided read-only on the live generation with the same checks the
staged route applies to its copy (shallow integrity, format version,
current coverage metadata and doc-link layer, byte-identical semantic
manifest); the run then publishes nothing and only refreshes the
artifact from the live database. Any other outcome copies and routes
exactly as before, so every change -- including a same-size edit that
keeps the original mtime -- still rebuilds from content hashes.
cbm_pipeline_new no longer resolves the Git context the run start
always re-resolved (seven git subprocesses per call).

No-op index_repository, macOS, median of three runs of n=3:
django 840.8 -> 539.0 ms, redis 450.7 -> 310.5 ms, Linux kernel
18449.5 -> 2405.0 ms. Responses and graphs are byte-identical to the
baseline for the no-op and for modify/add/delete/rename edits.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-14 20:29:55 +02:00
1 parent 7729a8ee9f
commit a372a1fa5e
4 files changed
+367 -40

No files matched your search

+119 -19
View File
@@ -260,6 +260,15 @@ struct cbm_pipeline {
* "invalid_existing_db" (#1864). */
bool final_existed;
bool existing_generation;
/* The copy into the stage is deferred until routing needs it: an
* unchanged repository is proven current on the live generation
* (live_db_path, read-only) and never copies, seals or renames a
* database. backup_succeeded records the copy once it ran; live_noop
* marks a run that ended on the live proof with nothing staged. */
bool stage_backup_pending;
bool backup_succeeded;
bool live_noop;
const char *live_db_path; /* borrowed from cbm_pipeline_run */
/* 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. */
@@ -848,7 +857,10 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
p->repo_path = strdup(repo_path);
p->db_path = db_path ? strdup(db_path) : NULL;
p->project_name = cbm_project_name_from_path(repo_path);
(void)cbm_git_context_resolve(repo_path, &p->git_ctx);
/* The Git context is resolved once, when the run starts
* (pipeline_refresh_git_context): a context resolved here was always
* replaced before any use, at the cost of seven git subprocesses per
* index call. Until then the branch QN names the working tree. */
p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx);
p->requested_mode = mode;
p->mode = mode;
@@ -2738,12 +2750,69 @@ static int capture_existing_adr(cbm_pipeline_t *p, const char *db_path) {
return 0;
}
static int remove_stage_files(const char *stage_path);
/* Copy the live generation into this run's stage: every route except the
* live no-op works on that copy. A failed copy behaves as the eager copy it
* replaces did: the stage files go (the name and its lock stay this run's,
* the rebuilt generation is renamed over it later) and the run has no
* previous generation, so it rebuilds in full. */
static void materialize_stage_backup(cbm_pipeline_t *p) {
p->stage_backup_pending = false;
p->backup_succeeded = cbm_store_backup_path(p->live_db_path, p->db_path) == CBM_STORE_OK;
if (!p->backup_succeeded) {
cbm_log_warn("pipeline.stage", "action", "backup_failed_full_rebuild", "path",
p->live_db_path);
(void)remove_stage_files(p->db_path);
}
p->existing_generation = p->backup_succeeded;
}
/* The live generation is provably current: read-only, it passes the shallow
* integrity and format checks the staged route applies to its copy, and the
* exact semantic-manifest test the staged route would answer no-op with. A
* copy of it would route to that no-op and publish the same database again,
* so the run ends here with nothing copied, sealed or renamed. Any doubt
* answers false and the run copies and routes exactly as before. */
static bool live_generation_current(cbm_pipeline_t *p, const cbm_file_hash_t *baseline_manifest,
int baseline_count) {
struct stat db_st;
if (!p->live_db_path || stat(p->live_db_path, &db_st) != 0) {
return false;
}
cbm_store_t *store = cbm_store_open_path_query(p->live_db_path);
if (!store) {
return false;
}
int fmt = 0;
bool current = cbm_store_check_integrity(store);
if (current) {
cbm_store_get_format_version(store, &fmt);
current =
fmt == CBM_INDEX_FORMAT_VERSION &&
cbm_pipeline_incremental_generation_exact(p, store, baseline_manifest, baseline_count);
}
cbm_store_close(store);
return current;
}
/* Route an existing generation. Full rebuilds never delete the live DB here:
* publication owns the eventual atomic replacement after every pass and
* metadata write has succeeded. */
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count,
const cbm_file_hash_t *baseline_manifest,
int baseline_count, bool force_full_on_mismatch) {
if (p->stage_backup_pending) {
if (live_generation_current(p, baseline_manifest, baseline_count)) {
p->stage_backup_pending = false;
p->live_noop = true;
cbm_log_info("pipeline.route", "path", "incremental_manifest", "generation", "live");
cbm_log_info("incremental.noop", "reason", "semantic_manifest_equal", "generation",
"live");
return 0;
}
materialize_stage_backup(p);
}
if (!p->existing_generation) {
/* Nothing to be incremental against: a first index, or a
* destination that could not be copied (already reported as
@@ -2846,12 +2915,15 @@ static bool promote_mode_to_existing_coverage(cbm_pipeline_t *p) {
if (!p || !p->project_name) {
return false;
}
char *db_path = resolve_db_path(p);
/* Until routing copies it, the stage is an empty placeholder: the
* coverage the copy would carry is read from the live generation. */
char *resolved = p->stage_backup_pending ? NULL : resolve_db_path(p);
const char *db_path = p->stage_backup_pending ? p->live_db_path : resolved;
if (!db_path) {
return false;
}
cbm_store_t *store = cbm_store_open_path_query(db_path);
free(db_path);
free(resolved);
if (!store) {
return false;
}
@@ -4542,6 +4614,29 @@ static void sweep_orphan_stages(const char *final_path) {
free(dir_path);
}
/* End a run whose exact no-op was proven on the live generation. Nothing was
* staged, so nothing is sealed or renamed and the live database stays the
* current generation. The checks publication makes on the run's sources and
* destination still decide success, and the artifact is refreshed from the
* live database exactly as it would be from a republished one. */
static int finish_live_noop(cbm_pipeline_t *p, const char *final_path) {
cbm_userconfig_t *fresh_config = cbm_userconfig_load_with_source(p->repo_path);
bool source_matches = cbm_pipeline_project_source_matches(p, fresh_config);
cbm_userconfig_free(fresh_config);
struct stat final_st;
int rc = 0;
if (!source_matches) {
cbm_log_error("pipeline.err", "phase", "test_declarations_source_changed");
rc = CBM_PIPELINE_ABORT_PRESERVE_DB;
} else if (stat(final_path, &final_st) != 0) {
/* The destination vanished during the run: someone else owns it. */
rc = CBM_PIPELINE_ABORT_PRESERVE_DB;
} else {
rc = export_after_publish(p, final_path);
}
return rc;
}
int cbm_pipeline_run(cbm_pipeline_t *p) {
/* Per-index attribution: peaks and phase totals are about THIS index, not
* the process history, so they start clean here. The first mark opens
@@ -4571,21 +4666,12 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
return CBM_NOT_FOUND;
}
bool backup_succeeded = false;
if (final_existed) {
backup_succeeded = cbm_store_backup_path(final_path, staging_path) == CBM_STORE_OK;
if (!backup_succeeded) {
cbm_log_warn("pipeline.stage", "action", "backup_failed_full_rebuild", "path",
final_path);
/* The copy is gone but the NAME stays ours: the rebuilt
* generation is renamed over it by the inner finalize and then
* published from it below, so its lock is held to the end. */
remove_stage_files(staging_path);
}
}
/* The existing generation is copied into the stage only when routing
* needs a copy (materialize_stage_backup): an exact no-op is proven on
* the live generation and copies nothing -- on a kernel-scale index the
* copy alone was a 19 GB write per unchanged re-index. */
p->final_existed = final_existed;
p->existing_generation = final_existed && backup_succeeded;
p->existing_generation = final_existed;
char *configured_db_path = p->db_path;
p->db_path = strdup(staging_path);
if (!p->db_path) {
@@ -4595,10 +4681,19 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
free(final_path);
return CBM_NOT_FOUND;
}
p->stage_backup_pending = final_existed;
p->backup_succeeded = false;
p->live_noop = false;
p->live_db_path = final_path;
cbm_log_debug("pipeline.setup", "step", "staged");
int rc = cbm_pipeline_run_staged(p);
free(p->db_path);
p->db_path = configured_db_path;
bool backup_succeeded = p->backup_succeeded;
bool live_noop = p->live_noop;
p->stage_backup_pending = false;
p->live_noop = false;
p->live_db_path = NULL;
/* Report WHY the run stopped. Everything below happens before the final
* rename, so the live database is still the previous generation and an
@@ -4612,12 +4707,17 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
pipeline_release_test_config(p);
return rc;
}
if (check_cancel(p)) {
/* A cancelled run and a live no-op both end here without publishing:
* the stage placeholder is discarded, and only the no-op goes on to
* finish on the live generation. */
bool cancelled = check_cancel(p);
if (cancelled || live_noop) {
cleanup_staging_db(staging_path);
free(staging_path);
rc = cancelled ? CBM_PIPELINE_ABORT_PRESERVE_DB : finish_live_noop(p, final_path);
free(final_path);
pipeline_release_test_config(p);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
return rc;
}
if (seal_staging_db(staging_path) != 0) {
cleanup_staging_db(staging_path);
+61 -15
View File
@@ -3557,6 +3557,62 @@ static bool incr_doc_links_current(cbm_store_t *store, const char *project) {
return current;
}
/* The exact no-op test: the stored generation's metadata is current for this
* mode, its doc-link layer is usable, and its recorded semantic manifest
* equals the baseline byte for byte. Read-only. The two out flags report the
* halves the staged route also needs for its own routing. */
static bool incr_generation_exact(cbm_pipeline_t *p, cbm_store_t *store, const char *project,
const cbm_file_hash_t *stored, int stored_count,
const cbm_file_hash_t *baseline_manifest, int baseline_count,
bool *metadata_current_out, bool *doc_links_current_out) {
cbm_coverage_meta_t meta = {0};
int meta_rc = cbm_store_coverage_meta_get(store, project, &meta);
const char *mode_name = incr_mode_name(cbm_pipeline_get_mode(p));
bool metadata_current =
meta_rc == CBM_STORE_OK && meta.coverage_version == CBM_SEMANTIC_INDEX_VERSION &&
meta.hash_records_complete && meta.index_mode && strcmp(meta.index_mode, mode_name) == 0;
/* A generation whose doc-link layer is missing or failed is not
* current even with identical inputs: index_status tells the user to
* re-run, and the re-run has to rebuild. */
bool doc_links_current = metadata_current && incr_doc_links_current(store, project);
bool exact = doc_links_current && cbm_pipeline_semantic_manifests_equal(
stored, stored_count, baseline_manifest, baseline_count);
cbm_store_coverage_meta_clear(&meta);
*metadata_current_out = metadata_current;
*doc_links_current_out = doc_links_current;
return exact;
}
bool cbm_pipeline_incremental_generation_exact(cbm_pipeline_t *p, cbm_store_t *store,
const cbm_file_hash_t *baseline_manifest,
int baseline_count) {
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
/* A forced legacy-partial run must reach the staged route that takes
* the one-shot flag; answering here would consume the run without it. */
if (atomic_load(&g_incr_test_force_legacy_partial)) {
return false;
}
#endif
const char *project = cbm_pipeline_project_name(p);
cbm_file_hash_t *stored = NULL;
int stored_count = 0;
if (!store ||
cbm_store_get_file_hashes(store, project, &stored, &stored_count) != CBM_STORE_OK) {
return false;
}
bool metadata_current = false;
bool doc_links_current = false;
bool exact = incr_generation_exact(p, store, project, stored, stored_count, baseline_manifest,
baseline_count, &metadata_current, &doc_links_current);
cbm_store_free_file_hashes(stored, stored_count);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (exact) {
incr_test_set_last_route(CBM_INCREMENTAL_ROUTE_NOOP);
}
#endif
return exact;
}
int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_file_info_t *files,
int file_count, const cbm_file_hash_t *baseline_manifest,
int baseline_count, bool force_full_on_mismatch) {
@@ -3602,21 +3658,11 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
* rebuilds the complete graph. This avoids partial LSP/package/config
* convergence gaps while retaining the common unchanged-repo fast path. */
if (!force_legacy_partial) {
cbm_coverage_meta_t meta = {0};
int meta_rc = cbm_store_coverage_meta_get(store, project, &meta);
const char *mode_name = incr_mode_name(cbm_pipeline_get_mode(p));
bool metadata_current = meta_rc == CBM_STORE_OK &&
meta.coverage_version == CBM_SEMANTIC_INDEX_VERSION &&
meta.hash_records_complete && meta.index_mode &&
strcmp(meta.index_mode, mode_name) == 0;
/* A generation whose doc-link layer is missing or failed is not
* current even with identical inputs: index_status tells the user to
* re-run, and the re-run has to rebuild. */
bool doc_links_current = metadata_current && incr_doc_links_current(store, project);
bool exact = doc_links_current &&
cbm_pipeline_semantic_manifests_equal(stored, stored_count, baseline_manifest,
baseline_count);
cbm_store_coverage_meta_clear(&meta);
bool metadata_current = false;
bool doc_links_current = false;
bool exact =
incr_generation_exact(p, store, project, stored, stored_count, baseline_manifest,
baseline_count, &metadata_current, &doc_links_current);
if (exact) {
cbm_store_free_file_hashes(stored, stored_count);
cbm_store_close(store);
+7
View File
@@ -1382,6 +1382,13 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
int file_count, const cbm_file_hash_t *baseline_manifest,
int baseline_count, bool force_full_on_mismatch);
/* The exact no-op test cbm_pipeline_run_incremental routes by, read-only on
* an already opened generation: true when that generation is current for the
* pipeline's mode and records exactly the baseline manifest. */
bool cbm_pipeline_incremental_generation_exact(cbm_pipeline_t *p, cbm_store_t *store,
const cbm_file_hash_t *baseline_manifest,
int baseline_count);
/* Exact semantic inputs for no-op/forced-full routing. The manifest contains
* every discovered source plus repository controls actually consumed by
* discovery, package mapping, path aliases, and extension overrides. */
+180 -6
View File
@@ -5759,6 +5759,177 @@ TEST(pipeline_concurrent_sweep_must_not_remove_inflight_stage) {
PASS();
}
static int live_noop_run(const char *repo, const char *db_path, cbm_incremental_route_t *route) {
cbm_pipeline_incremental_test_reset_faults();
*route = CBM_INCREMENTAL_ROUTE_NONE;
cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL);
if (!p) {
return CBM_NOT_FOUND;
}
int rc = cbm_pipeline_run(p);
*route = cbm_pipeline_incremental_test_last_route();
cbm_pipeline_free(p);
return rc;
}
/* An unchanged re-index proves the exact no-op on the LIVE generation. It
* never copies the database into a stage, seals the copy and renames it back
* (on a kernel-scale index that copy alone was a 19 GB write per call): the
* live file keeps its identity, size and mtime, no stage artifact remains,
* and the graph still serves. */
TEST(pipeline_unchanged_reindex_leaves_live_generation_untouched) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_live_noop_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp);
char *project = cbm_project_name_from_path(tmp);
ASSERT_NOT_NULL(project);
cbm_incremental_route_t first_route;
int first_rc = live_noop_run(tmp, db_path, &first_route);
cbm_path_info_t before = {0};
struct stat before_st;
bool before_ok =
cbm_path_info_utf8(db_path, &before) == CBM_PATH_INFO_OK && stat(db_path, &before_st) == 0;
cbm_incremental_route_t noop_route;
int noop_rc = live_noop_run(tmp, db_path, &noop_route);
cbm_path_info_t after = {0};
struct stat after_st;
bool after_ok =
cbm_path_info_utf8(db_path, &after) == CBM_PATH_INFO_OK && stat(db_path, &after_st) == 0;
int defined_count = -1;
int absent_count = -1;
observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &defined_count,
&absent_count);
int stage_count = count_generation_stage_artifacts(tmp, "generation.db");
cbm_pipeline_incremental_test_reset_faults();
free(project);
th_rmtree(tmp);
ASSERT_EQ(first_rc, 0);
ASSERT_TRUE(before_ok);
ASSERT_EQ(noop_rc, 0);
ASSERT_EQ(noop_route, CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_TRUE(after_ok);
ASSERT_TRUE(after_st.st_ino == before_st.st_ino);
ASSERT_TRUE(after.mtime_ns == before.mtime_ns);
ASSERT_TRUE(after.size == before.size);
ASSERT_EQ(stage_count, 0);
ASSERT_EQ(defined_count, 1);
ASSERT_EQ(absent_count, 0);
PASS();
}
/* The live proof answers only for an exact manifest. Every real change -- a
* same-size edit that keeps the original mtime, an addition, a deletion and
* a rename -- still routes past it into a rebuild that the graph shows, and
* only the unchanged re-index after them takes the no-op. */
TEST(pipeline_live_noop_proof_never_hides_a_change) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_live_noop_change_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
write_temp_file(tmp, "alpha.py", "def AlphaDefined():\n return 1\n");
write_temp_file(tmp, "beta.py", "def BetaDefined():\n return 2\n");
char db_path[512];
char alpha_path[512];
char beta_path[512];
char gamma_path[512];
char delta_path[512];
snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp);
snprintf(alpha_path, sizeof(alpha_path), "%s/alpha.py", tmp);
snprintf(beta_path, sizeof(beta_path), "%s/beta.py", tmp);
snprintf(gamma_path, sizeof(gamma_path), "%s/gamma.py", tmp);
snprintf(delta_path, sizeof(delta_path), "%s/delta.py", tmp);
char *project = cbm_project_name_from_path(tmp);
ASSERT_NOT_NULL(project);
cbm_incremental_route_t initial_route;
int initial_rc = live_noop_run(tmp, db_path, &initial_route);
/* Same size, original mtime restored: only the bytes differ. */
cbm_path_info_t alpha_info = {0};
bool alpha_stat_ok = cbm_path_info_utf8(alpha_path, &alpha_info) == CBM_PATH_INFO_OK;
write_temp_file(tmp, "alpha.py", "def AlphbDefined():\n return 1\n");
int mtime_rc = pipeline_test_set_mtime(alpha_path, (time_t)(alpha_info.mtime_ns / 1000000000),
(long)(alpha_info.mtime_ns % 1000000000));
cbm_path_info_t edited_info = {0};
bool edited_same_stat = cbm_path_info_utf8(alpha_path, &edited_info) == CBM_PATH_INFO_OK &&
edited_info.size == alpha_info.size &&
edited_info.mtime_ns == alpha_info.mtime_ns;
cbm_incremental_route_t modify_route;
int modify_rc = live_noop_run(tmp, db_path, &modify_route);
int old_alpha = -1;
int new_alpha = -1;
observe_named_generation(db_path, project, "AlphaDefined", "AlphbDefined", &old_alpha,
&new_alpha);
write_temp_file(tmp, "gamma.py", "def GammaDefined():\n return 3\n");
cbm_incremental_route_t add_route;
int add_rc = live_noop_run(tmp, db_path, &add_route);
int gamma_added = -1;
int unused = -1;
observe_named_generation(db_path, project, "GammaDefined", "NeverDefined", &gamma_added,
&unused);
int unlink_rc = cbm_unlink(beta_path);
cbm_incremental_route_t delete_route;
int delete_rc = live_noop_run(tmp, db_path, &delete_route);
int beta_left = -1;
observe_named_generation(db_path, project, "BetaDefined", "NeverDefined", &beta_left, &unused);
int rename_rc = cbm_rename_replace(gamma_path, delta_path);
cbm_incremental_route_t rename_route;
int renamed_rc = live_noop_run(tmp, db_path, &rename_route);
int gamma_renamed = -1;
observe_named_generation(db_path, project, "GammaDefined", "NeverDefined", &gamma_renamed,
&unused);
int old_path_hash = CBM_STORE_OK;
int new_path_hash = CBM_STORE_NOT_FOUND;
cbm_store_t *store = cbm_store_open_path_query(db_path);
if (store) {
cbm_file_hash_t hash = {0};
old_path_hash = cbm_store_get_file_hash(store, project, "gamma.py", &hash);
cbm_store_clear_file_hash(&hash);
new_path_hash = cbm_store_get_file_hash(store, project, "delta.py", &hash);
cbm_store_clear_file_hash(&hash);
cbm_store_close(store);
}
cbm_incremental_route_t final_route;
int final_rc = live_noop_run(tmp, db_path, &final_route);
cbm_pipeline_incremental_test_reset_faults();
free(project);
th_rmtree(tmp);
ASSERT_EQ(initial_rc, 0);
ASSERT_TRUE(alpha_stat_ok);
ASSERT_EQ(mtime_rc, 0);
ASSERT_TRUE(edited_same_stat);
ASSERT_EQ(modify_rc, 0);
ASSERT_TRUE(modify_route != CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(old_alpha, 0);
ASSERT_EQ(new_alpha, 1);
ASSERT_EQ(add_rc, 0);
ASSERT_TRUE(add_route != CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(gamma_added, 1);
ASSERT_EQ(unlink_rc, 0);
ASSERT_EQ(delete_rc, 0);
ASSERT_TRUE(delete_route != CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(beta_left, 0);
ASSERT_EQ(rename_rc, 0);
ASSERT_EQ(renamed_rc, 0);
ASSERT_TRUE(rename_route != CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(gamma_renamed, 1);
ASSERT_EQ(old_path_hash, CBM_STORE_NOT_FOUND);
ASSERT_EQ(new_path_hash, CBM_STORE_OK);
ASSERT_EQ(final_rc, 0);
ASSERT_EQ(final_route, CBM_INCREMENTAL_ROUTE_NOOP);
PASS();
}
static char g_route_log_capture[8192];
static atomic_flag g_route_log_spin = ATOMIC_FLAG_INIT;
@@ -6368,15 +6539,15 @@ TEST(pipeline_existing_artifact_refreshes_after_default_forced_full_reindex) {
ASSERT_EQ(reindex_observer.rename_calls, 1);
ASSERT_EQ(reindex_observer.exports_before_publish, 0);
ASSERT_EQ(reindex_observer.export_count, 1);
/* The no-ops are proven on the live generation and publish nothing; the
* artifact is still refreshed, from that already published database. */
ASSERT_EQ(explicit_rc, 0);
ASSERT_EQ(explicit_route, CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(explicit_observer.rename_calls, 1);
ASSERT_EQ(explicit_observer.exports_before_publish, 0);
ASSERT_EQ(explicit_observer.rename_calls, 0);
ASSERT_EQ(explicit_observer.export_count, 1);
ASSERT_EQ(unchanged_rc, 0);
ASSERT_EQ(unchanged_route, CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_EQ(observer.rename_calls, 1);
ASSERT_EQ(observer.exports_before_publish, 0);
ASSERT_EQ(observer.rename_calls, 0);
ASSERT_EQ(observer.export_count, 1);
ASSERT_TRUE(journal_ok);
ASSERT_STR_EQ(journal_mode, "delete");
@@ -18853,8 +19024,9 @@ TEST(incremental_downgrade_preserves_scope_and_artifact_across_change_noop_delet
ASSERT_EQ(noop.rc, 0);
ASSERT_EQ(noop.route, CBM_INCREMENTAL_ROUTE_NOOP);
ASSERT_TRUE(noop.tools_excluded);
ASSERT_EQ(noop.publish.rename_calls, 1);
ASSERT_EQ(noop.publish.exports_before_publish, 0);
/* Proven on the live generation: nothing is published, the artifact is
* still refreshed from the live database. */
ASSERT_EQ(noop.publish.rename_calls, 0);
ASSERT_EQ(noop.publish.export_count, 1);
ASSERT_EQ(deleted.rc, 0);
@@ -23352,6 +23524,8 @@ SUITE(pipeline_semantic_manifest_repro) {
RUN_TEST(pipeline_sweep_reclaims_orphans_of_other_projects_and_keeps_their_live_stages);
RUN_TEST(pipeline_sweep_removes_dead_writer_stage_keeps_locked_stage);
RUN_TEST(pipeline_concurrent_sweep_must_not_remove_inflight_stage);
RUN_TEST(pipeline_unchanged_reindex_leaves_live_generation_untouched);
RUN_TEST(pipeline_live_noop_proof_never_hides_a_change);
RUN_TEST(pipeline_fresh_index_never_reports_invalid_existing_db);
RUN_TEST(pipeline_source_mutation_before_publication_preserves_previous_generation);
RUN_TEST(pipeline_source_addition_before_publication_preserves_previous_generation);