mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
fix(pipeline): preserve overlay graph parity
Serialize call args consistently for call-derived edges across sequential and parallel indexing, including CONFIGURES edges used by MCP graph responses and data-flow derivation. Add a generic argument-list fallback for call extraction so grammars exposing arguments as argument_list still populate CBMCall.args. Guard exact delta and overlay publication against cross-file node-QN collisions, falling back instead of letting a changed header override a canonical source definition in the active overlay view. Validation: build/c test runner and binary rebuild passed; CBM_ONLY_SUITE=pipeline passed; lint-source-safety passed; full self-dogfood MCP matrix passed 5/5 in /private/tmp/cbm-cross-file-qn-guard-full-self-dogfood-20260704T.json. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -1144,6 +1144,14 @@ static char *gotemplate_string_child(CBMArena *a, TSNode parent, const char *sou
|
||||
return (char *)v;
|
||||
}
|
||||
|
||||
static TSNode find_call_arguments_node(TSNode node) {
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
if (ts_node_is_null(args)) {
|
||||
args = cbm_find_child_by_kind(node, "argument_list");
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
// Resolve a Go-template / Helm call to the referenced named template:
|
||||
// {{ template "x" . }} -> template_action, name is a string child
|
||||
// {{ include "x" . }} -> function_call(include), name is first string arg
|
||||
@@ -1163,10 +1171,7 @@ static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source) {
|
||||
strcmp(fname, "tpl") != 0)) {
|
||||
return NULL;
|
||||
}
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
if (ts_node_is_null(args)) {
|
||||
args = cbm_find_child_by_kind(node, "argument_list");
|
||||
}
|
||||
TSNode args = find_call_arguments_node(node);
|
||||
if (ts_node_is_null(args)) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -1713,7 +1718,7 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
|
||||
call.is_method = true;
|
||||
}
|
||||
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
TSNode args = find_call_arguments_node(node);
|
||||
if (!ts_node_is_null(args)) {
|
||||
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
|
||||
if (call.first_string_arg && call.first_string_arg[0] == '/') {
|
||||
|
||||
@@ -121,16 +121,17 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert an edge, using the shared CALLS finalizer so sequential containment
|
||||
* and parallel full indexing serialize call-site args/line identically. Keep
|
||||
* this restricted to CALLS: route/config edge props feed full-only predump
|
||||
* passes, so altering them desyncs full vs incremental indexing. */
|
||||
/* Insert a call-site edge through the shared finalizer so sequential and
|
||||
* parallel indexing serialize call args identically. Only plain CALLS carries
|
||||
* line metadata; service/config edges use args for derived route/data-flow
|
||||
* passes but keep their existing edge-specific fields as the stable contract. */
|
||||
static void calls_emit_edge(cbm_gbuf_t *gbuf, int64_t src, int64_t tgt, const char *type,
|
||||
char *props, size_t cap, const CBMCall *call) {
|
||||
if (call && strcmp(type, "CALLS") == 0) {
|
||||
if (call) {
|
||||
size_t len = strlen(props);
|
||||
if (len >= SKIP_ONE && props[len - SKIP_ONE] == '}') {
|
||||
cbm_pipeline_close_call_edge_props(props, cap, len - SKIP_ONE, call, true);
|
||||
bool include_line = type && strcmp(type, "CALLS") == 0;
|
||||
cbm_pipeline_close_call_edge_props(props, cap, len - SKIP_ONE, call, include_line);
|
||||
}
|
||||
}
|
||||
cbm_gbuf_insert_edge(gbuf, src, tgt, type, props);
|
||||
|
||||
@@ -35,6 +35,8 @@ static const char cbm_delta_reason_missing_existing_ownership[] = "missing_exist
|
||||
static const char cbm_delta_reason_missing_file_metadata[] = "missing_file_metadata";
|
||||
static const char cbm_delta_reason_preflight_error[] = CBM_PIPELINE_DELTA_REASON_PREFLIGHT_ERROR;
|
||||
static const char cbm_delta_reason_publish_error[] = "publish_error";
|
||||
static const char cbm_delta_reason_cross_file_node_qn_collision[] =
|
||||
CBM_PIPELINE_DELTA_REASON_CROSS_FILE_NODE_QN_COLLISION;
|
||||
static const char cbm_delta_reason_rename_requires_full[] = "rename_requires_full";
|
||||
static const char cbm_delta_reason_unresolved_edge_endpoint[] = "unresolved_edge_endpoint";
|
||||
static const char cbm_delta_reason_unsupported_derived_view[] = "unsupported_derived_view";
|
||||
@@ -875,6 +877,60 @@ static bool delta_existing_or_insert_ownership_supported(
|
||||
return true;
|
||||
}
|
||||
|
||||
int cbm_pipeline_file_delta_has_cross_file_node_qn_collision(
|
||||
cbm_store_t *store, const cbm_pipeline_file_delta_t *delta, bool *out_collision) {
|
||||
if (out_collision) {
|
||||
*out_collision = false;
|
||||
}
|
||||
if (!store || !delta || !delta->delta.project || !delta->delta.rel_path || !out_collision) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
if (delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
for (int i = 0; i < delta->delta.node_count; i++) {
|
||||
const cbm_node_t *node = &delta->delta.nodes[i];
|
||||
if (!node->qualified_name || node->qualified_name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
cbm_node_t existing = {0};
|
||||
int rc = cbm_store_find_node_by_qn(store, delta->delta.project, node->qualified_name,
|
||||
&existing);
|
||||
if (rc == CBM_STORE_NOT_FOUND) {
|
||||
continue;
|
||||
}
|
||||
if (rc != CBM_STORE_OK) {
|
||||
cbm_node_free_fields(&existing);
|
||||
return rc;
|
||||
}
|
||||
const char *node_file = node->file_path ? node->file_path : delta->delta.rel_path;
|
||||
bool collision = existing.file_path && node_file &&
|
||||
!delta_field_matches(existing.file_path, node_file);
|
||||
cbm_node_free_fields(&existing);
|
||||
if (collision) {
|
||||
*out_collision = true;
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
}
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
static bool delta_cross_file_node_qns_supported(cbm_store_t *store,
|
||||
const cbm_pipeline_file_delta_t *delta,
|
||||
cbm_pipeline_file_delta_plan_t *plan) {
|
||||
bool collision = false;
|
||||
int rc = cbm_pipeline_file_delta_has_cross_file_node_qn_collision(store, delta, &collision);
|
||||
if (rc != CBM_STORE_OK) {
|
||||
delta_plan_set_fallback(plan, cbm_delta_reason_preflight_error);
|
||||
return false;
|
||||
}
|
||||
if (collision) {
|
||||
delta_plan_set_fallback(plan, cbm_delta_reason_cross_file_node_qn_collision);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool delta_path_in_batch(const char *path, const cbm_pipeline_file_delta_t *const *deltas,
|
||||
int delta_count) {
|
||||
if (!path || !*path || !deltas) {
|
||||
@@ -1279,6 +1335,9 @@ int cbm_pipeline_plan_file_delta(cbm_store_t *store, const cbm_pipeline_file_del
|
||||
if (!delta_existing_or_insert_ownership_supported(store, delta, out)) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
if (!delta_cross_file_node_qns_supported(store, delta, out)) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
enum { CBM_DELTA_SINGLE_COUNT = 1 };
|
||||
const cbm_pipeline_file_delta_t *single_delta[] = {delta};
|
||||
if (!delta_inbound_edges_supported(store, delta, single_delta, CBM_DELTA_SINGLE_COUNT, out)) {
|
||||
@@ -1368,6 +1427,9 @@ int cbm_pipeline_plan_file_delta_batch(cbm_store_t *store,
|
||||
if (!delta_existing_or_insert_ownership_supported(store, delta, out)) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
if (!delta_cross_file_node_qns_supported(store, delta, out)) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
if (!delta_inbound_edges_supported(store, delta, deltas, delta_count, out)) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
@@ -1563,6 +1625,12 @@ int cbm_pipeline_publish_overlay_file_delta_batch(cbm_store_t *store,
|
||||
} else if (strcmp(project, deltas[i]->delta.project) != 0) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
bool collision = false;
|
||||
int rc =
|
||||
cbm_pipeline_file_delta_has_cross_file_node_qn_collision(store, deltas[i], &collision);
|
||||
if (rc != CBM_STORE_OK || collision) {
|
||||
return rc == CBM_STORE_OK ? CBM_STORE_NOT_FOUND : rc;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t overlay_generation = 0;
|
||||
|
||||
@@ -1577,6 +1577,21 @@ static int incr_try_overlay_upsert_route(cbm_pipeline_t *p, cbm_store_t *store,
|
||||
delta_ptrs[i] = &deltas[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < changed_count; i++) {
|
||||
bool collision = false;
|
||||
rc = cbm_pipeline_file_delta_has_cross_file_node_qn_collision(store, &deltas[i],
|
||||
&collision);
|
||||
if (rc != CBM_STORE_OK || collision) {
|
||||
const char *reason =
|
||||
collision ? CBM_PIPELINE_DELTA_REASON_CROSS_FILE_NODE_QN_COLLISION
|
||||
: CBM_PIPELINE_DELTA_REASON_PREFLIGHT_ERROR;
|
||||
cbm_pipeline_set_publish_reason(p, reason);
|
||||
cbm_log_info("incremental.overlay.fallback", "reason", reason, "rc",
|
||||
itoa_buf_incr(rc));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
rc = cbm_store_latest_complete_index_generation(store, project, &base_generation);
|
||||
if (rc == CBM_STORE_OK) {
|
||||
CBM_PROF_START(t_overlay_publish);
|
||||
|
||||
@@ -348,6 +348,7 @@ typedef struct {
|
||||
#define CBM_PIPELINE_DELTA_REASON_FRONTIER_TOO_LARGE "frontier_too_large"
|
||||
#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"
|
||||
|
||||
/* Conservative default exact-delta caps. Larger affected sets fall back to the
|
||||
* containment path unless config opts into a benchmarked frontier size. */
|
||||
@@ -440,6 +441,8 @@ int cbm_pipeline_build_file_delta_from_gbuf(const cbm_gbuf_t *gbuf, const char *
|
||||
const char *rel_path, int64_t generation,
|
||||
cbm_pipeline_file_delta_t *out);
|
||||
bool cbm_pipeline_delta_edge_type_is_recomputed(const char *type);
|
||||
int cbm_pipeline_file_delta_has_cross_file_node_qn_collision(
|
||||
cbm_store_t *store, const cbm_pipeline_file_delta_t *delta, bool *out_collision);
|
||||
int cbm_pipeline_file_delta_add_preserved_inbound_edges(cbm_store_t *store,
|
||||
cbm_pipeline_file_delta_t *delta,
|
||||
int *out_added);
|
||||
|
||||
+83
-4
@@ -2011,9 +2011,10 @@ static int pipeline_dump_store_file_to_file(const char *src_path, const char *de
|
||||
return rc;
|
||||
}
|
||||
|
||||
static bool pipeline_store_has_edge_between_qns(const char *db_path, const char *project,
|
||||
const char *source_qn, const char *type,
|
||||
const char *target_qn) {
|
||||
static bool pipeline_store_edge_between_qns_matches(const char *db_path, const char *project,
|
||||
const char *source_qn, const char *type,
|
||||
const char *target_qn,
|
||||
const char *props_needle) {
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
return false;
|
||||
@@ -2029,7 +2030,9 @@ static bool pipeline_store_has_edge_between_qns(const char *db_path, const char
|
||||
if (cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &edge_count) ==
|
||||
CBM_STORE_OK) {
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
if (edges[i].target_id == tgt.id) {
|
||||
if (edges[i].target_id == tgt.id &&
|
||||
(!props_needle || (edges[i].properties_json &&
|
||||
strstr(edges[i].properties_json, props_needle)))) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -2038,10 +2041,19 @@ static bool pipeline_store_has_edge_between_qns(const char *db_path, const char
|
||||
}
|
||||
}
|
||||
|
||||
cbm_node_free_fields(&src);
|
||||
cbm_node_free_fields(&tgt);
|
||||
cbm_store_close(s);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool pipeline_store_has_edge_between_qns(const char *db_path, const char *project,
|
||||
const char *source_qn, const char *type,
|
||||
const char *target_qn) {
|
||||
return pipeline_store_edge_between_qns_matches(db_path, project, source_qn, type, target_qn,
|
||||
NULL);
|
||||
}
|
||||
|
||||
static void pipeline_restore_workers_env(bool had_workers, const char *saved_workers) {
|
||||
if (had_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
@@ -2497,8 +2509,19 @@ TEST(pipeline_parallel_env_access_matches_sequential) {
|
||||
|
||||
files[0] = "src/env.c";
|
||||
contents[0] = "#include <stdlib.h>\n\n"
|
||||
"const char *cbm_safe_getenv(const char *key, char *buf, unsigned long cap, "
|
||||
"void *err) {\n"
|
||||
" (void)buf;\n"
|
||||
" (void)cap;\n"
|
||||
" (void)err;\n"
|
||||
" return key;\n"
|
||||
"}\n\n"
|
||||
"const char *load_temp(void) {\n"
|
||||
" return getenv(\"CBM_TEST_PARALLEL_ENV\");\n"
|
||||
"}\n\n"
|
||||
"const char *load_home(void) {\n"
|
||||
" char buf[32];\n"
|
||||
" return cbm_safe_getenv(\"HOME\", buf, sizeof(buf), NULL);\n"
|
||||
"}\n";
|
||||
for (int i = 0; i < FILLER_FILE_COUNT; i++) {
|
||||
int rn = snprintf(filler_files[i], sizeof(filler_files[i]), "fillers/filler_%02d.c", i);
|
||||
@@ -2548,6 +2571,16 @@ TEST(pipeline_parallel_env_access_matches_sequential) {
|
||||
env_qn));
|
||||
ASSERT_TRUE(pipeline_store_has_edge_between_qns(par_db, project, source_qn, "CONFIGURES",
|
||||
env_qn));
|
||||
char call_target_qn[CBM_SZ_512];
|
||||
n = snprintf(call_target_qn, sizeof(call_target_qn), "%s.src.env.cbm_safe_getenv", project);
|
||||
ASSERT_GT(n, 0);
|
||||
ASSERT_LT((size_t)n, sizeof(call_target_qn));
|
||||
ASSERT_TRUE(pipeline_store_edge_between_qns_matches(seq_db, project, source_qn,
|
||||
"CONFIGURES", call_target_qn,
|
||||
"\"args\":["));
|
||||
ASSERT_TRUE(pipeline_store_edge_between_qns_matches(par_db, project, source_qn,
|
||||
"CONFIGURES", call_target_qn,
|
||||
"\"args\":["));
|
||||
|
||||
char diff_err[CBM_SZ_8K] = {0};
|
||||
int diff_rc = cbm_test_compare_canonical_graphs(seq_db, par_db, project, diff_err,
|
||||
@@ -3614,6 +3647,51 @@ static int pipeline_delta_store_qn_exists(cbm_store_t *s, const char *project, c
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(pipeline_file_delta_detects_cross_file_node_qn_collision) {
|
||||
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_node_t existing = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "Thing",
|
||||
.qualified_name = "test.src.store.store.Thing",
|
||||
.file_path = "src/store/store.c",
|
||||
.start_line = 1,
|
||||
.end_line = 4,
|
||||
.properties_json = "{}"};
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &existing), 0);
|
||||
|
||||
cbm_node_t delta_node = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "Thing",
|
||||
.qualified_name = "test.src.store.store.Thing",
|
||||
.file_path = "src/store/store.h",
|
||||
.start_line = 1,
|
||||
.end_line = 1,
|
||||
.properties_json = "{}"};
|
||||
cbm_pipeline_file_delta_t delta = {
|
||||
.delta = {.project = "test", .rel_path = "src/store/store.h", .node_count = 1},
|
||||
.nodes = &delta_node,
|
||||
.change_kind = CBM_PIPELINE_DELTA_CHANGE_UPSERT,
|
||||
};
|
||||
delta.delta.nodes = &delta_node;
|
||||
|
||||
bool collision = false;
|
||||
ASSERT_EQ(cbm_pipeline_file_delta_has_cross_file_node_qn_collision(s, &delta, &collision),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_TRUE(collision);
|
||||
|
||||
delta_node.file_path = "src/store/store.c";
|
||||
collision = true;
|
||||
ASSERT_EQ(cbm_pipeline_file_delta_has_cross_file_node_qn_collision(s, &delta, &collision),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_FALSE(collision);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void pipeline_delta_attach_test_metadata(cbm_pipeline_file_delta_t *delta,
|
||||
cbm_file_hash_t *hash,
|
||||
cbm_file_state_t *state) {
|
||||
@@ -13186,6 +13264,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_file_delta_scratch_seed_preserves_structure_roots);
|
||||
RUN_TEST(pipeline_file_delta_scratch_seed_supports_external_endpoint_descriptor);
|
||||
RUN_TEST(pipeline_file_delta_descriptor_from_gbuf);
|
||||
RUN_TEST(pipeline_file_delta_detects_cross_file_node_qn_collision);
|
||||
RUN_TEST(pipeline_file_delta_preserves_safe_inbound_edges_for_overlay);
|
||||
RUN_TEST(pipeline_file_delta_descriptor_marks_unsupported_edges);
|
||||
RUN_TEST(pipeline_file_delta_metadata_from_file);
|
||||
|
||||
Reference in New Issue
Block a user