diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index bb688d39b..cf7931b7f 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -85,6 +85,54 @@ static bool incr_changed_contains_c_family_header(const cbm_file_info_t *changed return false; } +static bool incr_file_delta_has_type_like_node(const cbm_pipeline_file_delta_t *delta) { + if (!delta || delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) { + return false; + } + for (int i = 0; i < delta->delta.node_count; i++) { + const cbm_node_t *node = &delta->delta.nodes[i]; + if (cbm_label_is_type_like(node->label)) { + return true; + } + } + return false; +} + +static bool incr_same_stem_impl_exists(const char *path) { + static const char *const impl_exts[] = {".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".cu"}; + if (!path || !path[0]) { + return false; + } + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) { + return false; + } + size_t stem_len = (size_t)(dot - path); + if (stem_len >= CBM_PATH_MAX || stem_len > (size_t)INT_MAX) { + return false; + } + for (size_t i = 0; i < sizeof(impl_exts) / sizeof(impl_exts[0]); i++) { + char candidate[CBM_PATH_MAX]; + int n = snprintf(candidate, sizeof(candidate), "%.*s%s", (int)stem_len, path, + impl_exts[i]); + if (n < 0 || (size_t)n >= sizeof(candidate)) { + continue; + } + if (cbm_file_exists(candidate)) { + return true; + } + } + return false; +} + +static bool incr_header_overlay_has_type_impl_pair(const cbm_file_info_t *file, + const cbm_pipeline_file_delta_t *delta) { + if (!file || !incr_is_c_family_header(file->language, file->rel_path)) { + return false; + } + return incr_file_delta_has_type_like_node(delta) && incr_same_stem_impl_exists(file->path); +} + /* ── File classification ─────────────────────────────────────────── */ /* Classify discovered files against stored metadata. @@ -1563,6 +1611,12 @@ static int incr_try_overlay_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, itoa_buf_incr(rc)); goto cleanup; } + if (incr_header_overlay_has_type_impl_pair(&changed_files[i], &deltas[i])) { + cbm_pipeline_set_publish_reason(p, CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR); + cbm_log_info("incremental.overlay.fallback", "reason", + CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR); + goto cleanup; + } int preserved = 0; rc = cbm_pipeline_file_delta_add_preserved_inbound_edges(store, &deltas[i], &preserved); if (rc != CBM_STORE_OK) { diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 7ed00fb85..5eb8e999d 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -349,6 +349,7 @@ typedef struct { #define CBM_PIPELINE_DELTA_REASON_INBOUND_EDGES_REQUIRE_FULL "inbound_edges_require_full" #define CBM_PIPELINE_DELTA_REASON_PREFLIGHT_ERROR "preflight_error" #define CBM_PIPELINE_DELTA_REASON_CROSS_FILE_NODE_QN_COLLISION "cross_file_node_qn_collision" +#define CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR "header_type_impl_pair" /* Conservative default exact-delta caps. Larger affected sets fall back to the * containment path unless config opts into a benchmarked frontier size. */ diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 73f427829..0a4789e6a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10212,6 +10212,73 @@ TEST(incremental_overlay_publish_single_c_header_uses_active_overlay) { PASS(); } +TEST(incremental_overlay_single_c_header_type_impl_pair_falls_back) { + if (setup_incremental_repo() != 0) { + FAIL("setup failed"); + } + + char header_path[CBM_PATH_MAX]; + char source_path[CBM_PATH_MAX]; + int n = snprintf(header_path, sizeof(header_path), "%s/paired.h", g_incr_tmpdir); + ASSERT(n >= 0 && (size_t)n < sizeof(header_path)); + n = snprintf(source_path, sizeof(source_path), "%s/paired.c", g_incr_tmpdir); + ASSERT(n >= 0 && (size_t)n < sizeof(source_path)); + ASSERT_EQ(th_write_file(header_path, + "#ifndef PAIRED_H\n" + "#define PAIRED_H\n" + "typedef struct Paired Paired;\n" + "int paired_value(Paired *p);\n" + "#endif\n"), + 0); + ASSERT_EQ(th_write_file(source_path, + "#include \"paired.h\"\n\n" + "struct Paired {\n" + " int value;\n" + "};\n\n" + "int paired_value(Paired *p) {\n" + " return p ? p->value : 0;\n" + "}\n"), + 0); + + cbm_config_t *cfg = incremental_test_config(g_incr_tmpdir); + ASSERT_NOT_NULL(cfg); + ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_OVERLAY_PUBLISH, + CBM_CONFIG_OVERLAY_PUBLISH_SMALL_DELTAS), + 0); + cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST); + ASSERT_NOT_NULL(p); + cbm_pipeline_apply_config(p, cfg); + ASSERT_EQ(cbm_pipeline_run(p), 0); + cbm_pipeline_free(p); + + ASSERT_EQ(th_write_file(header_path, + "#ifndef PAIRED_H\n" + "#define PAIRED_H\n" + "typedef struct Paired Paired;\n" + "int paired_value(Paired *p);\n" + "static int paired_extra(void) {\n" + " return 7;\n" + "}\n" + "#endif\n"), + 0); + + p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST); + ASSERT_NOT_NULL(p); + cbm_pipeline_apply_config(p, cfg); + pipeline_capture_logs_start(); + int run_rc = cbm_pipeline_run(p); + const char *logs = pipeline_capture_logs_end(); + ASSERT_EQ(run_rc, 0); + ASSERT(strstr(logs, "msg=incremental.overlay.fallback reason=" + CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR) != NULL); + ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT); + cbm_pipeline_free(p); + + cbm_config_close(cfg); + cleanup_incremental_repo(); + PASS(); +} + TEST(incremental_c_header_uses_exact_not_overlay_until_active_edges_are_exact) { enum { PIPELINE_C_HEADER_OVERLAY_MAX_AFFECTED = CBM_SZ_16, @@ -13761,6 +13828,7 @@ SUITE(pipeline) { RUN_TEST(incremental_fast_falls_back_for_oversized_inbound_frontier_and_matches_full); RUN_TEST(incremental_fast_c_header_frontier_too_large_uses_full_rebuild); RUN_TEST(incremental_overlay_publish_single_c_header_uses_active_overlay); + RUN_TEST(incremental_overlay_single_c_header_type_impl_pair_falls_back); RUN_TEST(incremental_c_header_uses_exact_not_overlay_until_active_edges_are_exact); RUN_TEST(incremental_fast_configured_frontier_cap_allows_bounded_exact); RUN_TEST(incremental_fast_mixed_unowned_edge_frontier_falls_back_before_exact_build);