mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-29 07:13:44 +08:00
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:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user