feat(pipeline): require metadata for delta planning

Harden the internal exact-delta planner so candidates require file_hash and file_state metadata before any future publish route can be considered.

Add explicit delete and rename fallback reasons because exact cleanup semantics for old paths, owner rows, reverse importers, and freshness state are not implemented yet.

Add focused planner tests for metadata-required candidates, missing metadata fallback, delete fallback, and rename fallback. This does not change incremental routing, defaults, or publish behavior.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-06-30 12:36:26 -04:00
parent 92de9438f2
commit 079b1be892
3 changed files with 120 additions and 0 deletions
+29
View File
@@ -10,9 +10,12 @@
static const char cbm_delta_edge_imports[] = "IMPORTS";
static const char cbm_delta_prop_is_exported[] = "is_exported";
static const char cbm_delta_reason_candidate[] = "candidate";
static const char cbm_delta_reason_delete_requires_full[] = "delete_requires_full";
static const char cbm_delta_reason_frontier_error[] = "frontier_error";
static const char cbm_delta_reason_frontier_too_large[] = "frontier_too_large";
static const char cbm_delta_reason_invalid_input[] = "invalid_input";
static const char cbm_delta_reason_missing_file_metadata[] = "missing_file_metadata";
static const char cbm_delta_reason_rename_requires_full[] = "rename_requires_full";
static const char cbm_delta_reason_unsupported_edges[] = "unsupported_edges";
enum { CBM_DELTA_GROWTH = 2 };
@@ -248,6 +251,20 @@ static void delta_plan_set_fallback(cbm_pipeline_file_delta_plan_t *plan, const
plan->reason = reason;
}
static bool delta_field_matches(const char *actual, const char *expected) {
return actual && expected && strcmp(actual, expected) == 0;
}
static bool delta_file_metadata_complete(const cbm_store_file_delta_t *delta) {
return delta && delta->file_hash && delta->file_state &&
delta_field_matches(delta->file_hash->project, delta->project) &&
delta_field_matches(delta->file_hash->rel_path, delta->rel_path) &&
delta->file_hash->sha256 &&
delta_field_matches(delta->file_state->project, delta->project) &&
delta_field_matches(delta->file_state->rel_path, delta->rel_path) &&
delta->file_state->content_hash && delta->file_state->indexed_at;
}
int cbm_pipeline_plan_file_delta(cbm_store_t *store, const cbm_pipeline_file_delta_t *delta,
int max_affected_paths, cbm_pipeline_file_delta_plan_t *out) {
if (!out) {
@@ -259,10 +276,22 @@ int cbm_pipeline_plan_file_delta(cbm_store_t *store, const cbm_pipeline_file_del
max_affected_paths <= 0) {
return CBM_STORE_OK;
}
if (delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) {
delta_plan_set_fallback(out, cbm_delta_reason_delete_requires_full);
return CBM_STORE_OK;
}
if (delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_RENAME) {
delta_plan_set_fallback(out, cbm_delta_reason_rename_requires_full);
return CBM_STORE_OK;
}
if (delta->unsupported_edge_count > 0) {
delta_plan_set_fallback(out, cbm_delta_reason_unsupported_edges);
return CBM_STORE_OK;
}
if (!delta_file_metadata_complete(&delta->delta)) {
delta_plan_set_fallback(out, cbm_delta_reason_missing_file_metadata);
return CBM_STORE_OK;
}
const char **new_export_qns = NULL;
if (delta->delta.export_count > 0) {
+6
View File
@@ -118,6 +118,12 @@ typedef struct {
cbm_store_symbol_export_t *exports;
cbm_store_import_ref_t *imports;
int unsupported_edge_count;
enum {
CBM_PIPELINE_DELTA_CHANGE_UPSERT = 0,
CBM_PIPELINE_DELTA_CHANGE_DELETE = 1,
CBM_PIPELINE_DELTA_CHANGE_RENAME = 2,
} change_kind;
const char *old_rel_path; /* borrowed; set for rename preflight only */
} cbm_pipeline_file_delta_t;
typedef enum {
+85
View File
@@ -3170,6 +3170,30 @@ static int pipeline_delta_plan_contains_path(const cbm_pipeline_file_delta_plan_
return 0;
}
static void pipeline_delta_attach_test_metadata(cbm_pipeline_file_delta_t *delta,
cbm_file_hash_t *hash,
cbm_file_state_t *state) {
enum { PIPELINE_DELTA_TEST_GENERATION = 1 };
*hash = (cbm_file_hash_t){.project = delta->delta.project,
.rel_path = delta->delta.rel_path,
.sha256 = "test-hash",
.mtime_ns = 1,
.size = 10};
*state = (cbm_file_state_t){.project = delta->delta.project,
.rel_path = delta->delta.rel_path,
.content_hash = "test-content",
.git_oid = "",
.mtime_ns = 1,
.size = 10,
.language = "go",
.pass_fingerprint = "test-pass",
.generation = PIPELINE_DELTA_TEST_GENERATION,
.indexed_at = "2026-06-30T00:00:00Z"};
delta->delta.generation = PIPELINE_DELTA_TEST_GENERATION;
delta->delta.file_hash = hash;
delta->delta.file_state = state;
}
TEST(pipeline_file_delta_descriptor_from_gbuf) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/proj");
ASSERT_NOT_NULL(gb);
@@ -3250,6 +3274,9 @@ TEST(pipeline_file_delta_plan_candidate_from_frontier) {
{.qualified_name = "test.lib.Value", .node_id = CBM_STORE_NO_NODE_ID}};
cbm_pipeline_file_delta_t delta = {
.delta = {.project = "test", .rel_path = "lib.go", .exports = exports, .export_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);
@@ -3263,6 +3290,23 @@ TEST(pipeline_file_delta_plan_candidate_from_frontier) {
PASS();
}
TEST(pipeline_file_delta_plan_falls_back_without_file_metadata) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "main.go"}};
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, "missing_file_metadata");
ASSERT_EQ(plan.affected_count, 0);
cbm_pipeline_file_delta_plan_free(&plan);
cbm_store_close(s);
PASS();
}
TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
@@ -3280,6 +3324,41 @@ TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges) {
PASS();
}
TEST(pipeline_file_delta_plan_falls_back_on_delete) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "gone.go"},
.change_kind = CBM_PIPELINE_DELTA_CHANGE_DELETE};
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, "delete_requires_full");
ASSERT_EQ(plan.affected_count, 0);
cbm_pipeline_file_delta_plan_free(&plan);
cbm_store_close(s);
PASS();
}
TEST(pipeline_file_delta_plan_falls_back_on_rename) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "new.go"},
.change_kind = CBM_PIPELINE_DELTA_CHANGE_RENAME,
.old_rel_path = "old.go"};
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, "rename_requires_full");
ASSERT_EQ(plan.affected_count, 0);
cbm_pipeline_file_delta_plan_free(&plan);
cbm_store_close(s);
PASS();
}
TEST(pipeline_file_delta_plan_falls_back_on_large_frontier) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
@@ -3298,6 +3377,9 @@ TEST(pipeline_file_delta_plan_falls_back_on_large_frontier) {
{.qualified_name = "test.lib.Hot", .node_id = CBM_STORE_NO_NODE_ID}};
cbm_pipeline_file_delta_t delta = {
.delta = {.project = "test", .rel_path = "lib.go", .exports = exports, .export_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_2, &plan), CBM_STORE_OK);
@@ -7647,7 +7729,10 @@ SUITE(pipeline) {
RUN_TEST(pipeline_file_delta_descriptor_from_gbuf);
RUN_TEST(pipeline_file_delta_descriptor_marks_unsupported_edges);
RUN_TEST(pipeline_file_delta_plan_candidate_from_frontier);
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);
RUN_TEST(pipeline_file_delta_plan_falls_back_on_rename);
RUN_TEST(pipeline_file_delta_plan_falls_back_on_large_frontier);
/* File persistence */
RUN_TEST(store_file_persistence);