diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 640ef56e5..3d9578817 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -905,11 +905,11 @@ static bool delta_batch_contains_edge(const cbm_pipeline_file_delta_t *const *de return false; } -static bool delta_unowned_inbound_edge_is_regenerated( +static bool delta_inbound_edge_is_regenerated_by_batch( const cbm_store_inbound_edge_t *edge, const cbm_pipeline_file_delta_t *const *deltas, int delta_count) { - return edge && edge->source_rel_path && edge->source_rel_path[0] == '\0' && edge->type && - strcmp(edge->type, cbm_delta_edge_contains_file) == 0 && + return edge && edge->source_rel_path && edge->source_rel_path[0] == '\0' && + delta_path_in_batch(edge->edge_rel_path, deltas, delta_count) && delta_batch_contains_edge(deltas, delta_count, edge->source_qn, edge->target_qn, edge->type); } @@ -936,7 +936,7 @@ static bool delta_inbound_edges_supported(cbm_store_t *store, bool ok = true; for (int i = 0; i < edge_count; i++) { if (!delta_path_in_batch(edges[i].source_rel_path, deltas, delta_count) && - !delta_unowned_inbound_edge_is_regenerated(&edges[i], deltas, delta_count) && + !delta_inbound_edge_is_regenerated_by_batch(&edges[i], deltas, delta_count) && !delta_owned_inbound_edge_is_deleted(&edges[i], delta)) { ok = false; break; diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index f33238021..e91fd42fc 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1022,6 +1022,13 @@ static bool incr_empty_source_inbound_edge_is_structure(const cbm_store_inbound_ strcmp(edge->type, CBM_PIPELINE_EDGE_CONTAINS_FILE) == 0; } +static bool incr_inbound_edge_owner_is_exact_file(const cbm_store_inbound_edge_t *edge, + const cbm_file_info_t *exact_files, + int exact_count) { + return edge && edge->source_rel_path && edge->source_rel_path[0] == '\0' && + incr_file_info_has_rel_path(exact_files, exact_count, edge->edge_rel_path); +} + static int incr_expand_exact_inbound_frontier(cbm_store_t *store, const char *project, const cbm_file_info_t *all_files, int all_file_count, cbm_file_info_t *exact_files, int *exact_count, @@ -1052,7 +1059,8 @@ static int incr_expand_exact_inbound_frontier(cbm_store_t *store, const char *pr for (int i = 0; i < edge_count; i++) { const char *source_rel_path = edges[i].source_rel_path; if (!source_rel_path || source_rel_path[0] == '\0') { - if (incr_empty_source_inbound_edge_is_structure(&edges[i])) { + if (incr_empty_source_inbound_edge_is_structure(&edges[i]) || + incr_inbound_edge_owner_is_exact_file(&edges[i], exact_files, *exact_count)) { continue; } if (out_reason) { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index a9779cc5c..9f4d5d31d 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3548,6 +3548,37 @@ static int pipeline_delta_seed_existing_ownership(cbm_store_t *s, const char *pr : CBM_STORE_ERR; } +static int pipeline_delta_seed_file_owned_unowned_source_edge( + cbm_store_t *s, const char *project, const char *rel_path, const char *source_qn, + const char *target_qn, const char *edge_type) { + enum { PIPELINE_DELTA_TEST_BASE_GENERATION = 1 }; + int64_t target_id = pipeline_delta_seed_existing_ownership_id(s, project, rel_path, target_qn); + if (target_id <= CBM_STORE_NO_NODE_ID) { + return CBM_STORE_ERR; + } + cbm_node_t source = {.project = (char *)project, + .label = "Module", + .name = "module", + .qualified_name = (char *)source_qn, + .file_path = "", + .properties_json = "{}"}; + int64_t source_id = cbm_store_upsert_node(s, &source); + if (source_id <= CBM_STORE_NO_NODE_ID) { + return CBM_STORE_ERR; + } + cbm_edge_t edge = {.project = (char *)project, + .source_id = source_id, + .target_id = target_id, + .type = (char *)edge_type, + .properties_json = "{}"}; + int64_t edge_id = cbm_store_insert_edge(s, &edge); + if (edge_id <= CBM_STORE_NO_NODE_ID) { + return CBM_STORE_ERR; + } + return cbm_store_upsert_edge_owner(s, project, edge_id, rel_path, NULL, + PIPELINE_DELTA_TEST_BASE_GENERATION); +} + static int pipeline_delta_seed_project_node(cbm_store_t *s, const char *project) { cbm_node_t project_node = {.project = (char *)project, .label = "Project", @@ -4925,6 +4956,86 @@ TEST(pipeline_file_delta_plan_accepts_regenerated_structural_inbound_edge) { PASS(); } +TEST(pipeline_file_delta_plan_accepts_regenerated_file_owned_unowned_source_edge) { + const char *project = "test"; + const char *rel_path = "src/main.go"; + const char *source_qn = "test.src.module"; + const char *target_qn = "test.src.main.Run"; + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + ASSERT_EQ(cbm_store_upsert_project(s, project, "/tmp/test"), CBM_STORE_OK); + ASSERT_EQ(pipeline_delta_seed_file_owned_unowned_source_edge( + s, project, rel_path, source_qn, target_qn, "CALLS"), + CBM_STORE_OK); + + cbm_node_t replacement_node = {.project = (char *)project, + .label = "Function", + .name = "Run", + .qualified_name = (char *)target_qn, + .file_path = (char *)rel_path, + .properties_json = "{}"}; + cbm_store_delta_edge_t replacement_edge = {.source_qn = source_qn, + .target_qn = target_qn, + .type = "CALLS", + .properties_json = "{}", + .derived_kind = CBM_STORE_DERIVED_KIND_DIRECT}; + cbm_pipeline_file_delta_t delta = {.delta = {.project = project, + .rel_path = rel_path, + .nodes = &replacement_node, + .node_count = 1, + .edges = &replacement_edge, + .edge_count = 1}}; + cbm_file_hash_t hash = {0}; + cbm_file_state_t state = {0}; + pipeline_delta_attach_test_metadata(&delta, &hash, &state); + + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_EXACT_CANDIDATE); + ASSERT_STR_EQ(plan.reason, "candidate"); + ASSERT_EQ(pipeline_delta_plan_contains_path(&plan, rel_path), 1); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + +TEST(pipeline_file_delta_plan_falls_back_on_stale_file_owned_unowned_source_edge) { + const char *project = "test"; + const char *rel_path = "src/main.go"; + const char *source_qn = "test.src.module"; + const char *target_qn = "test.src.main.Run"; + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + ASSERT_EQ(cbm_store_upsert_project(s, project, "/tmp/test"), CBM_STORE_OK); + ASSERT_EQ(pipeline_delta_seed_file_owned_unowned_source_edge( + s, project, rel_path, source_qn, target_qn, "CALLS"), + CBM_STORE_OK); + + cbm_node_t replacement_node = {.project = (char *)project, + .label = "Function", + .name = "Run", + .qualified_name = (char *)target_qn, + .file_path = (char *)rel_path, + .properties_json = "{}"}; + cbm_pipeline_file_delta_t delta = {.delta = {.project = project, + .rel_path = rel_path, + .nodes = &replacement_node, + .node_count = 1}}; + cbm_file_hash_t hash = {0}; + cbm_file_state_t state = {0}; + pipeline_delta_attach_test_metadata(&delta, &hash, &state); + + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_FALLBACK); + ASSERT_STR_EQ(plan.reason, "inbound_edges_require_full"); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + TEST(pipeline_file_delta_plan_falls_back_without_file_metadata) { cbm_store_t *s = cbm_store_open_memory(); ASSERT_NOT_NULL(s); @@ -11920,6 +12031,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_file_delta_plan_falls_back_on_new_folder_structure_edge); RUN_TEST(pipeline_file_delta_apply_inserts_and_prunes_new_folder_context); RUN_TEST(pipeline_file_delta_plan_accepts_regenerated_structural_inbound_edge); + RUN_TEST(pipeline_file_delta_plan_accepts_regenerated_file_owned_unowned_source_edge); + RUN_TEST(pipeline_file_delta_plan_falls_back_on_stale_file_owned_unowned_source_edge); RUN_TEST(pipeline_file_delta_plan_falls_back_without_file_metadata); RUN_TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges); RUN_TEST(pipeline_file_delta_plan_falls_back_on_delete);