mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(pipeline): suppress weak external lsp fallbacks
When LSP resolves a call to an external or unindexed target, keep strong registry evidence such as same-module/import-map matches but drop weak non-import-reachable short-name fallbacks. This prevents false CALLS edges like scope.get -> local *.get while preserving Python re-export/import behavior such as fastapi.Header. Add a focused pipeline canary for external LSP fallback suppression and keep the existing Python re-export and super().__init__ canaries green. FastAPI matrix remains correctness-safe through the scoped_lsp_gap full-reindex guard, so this is not a default-performance closure. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -298,6 +298,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
/* LSP-resolved calls take precedence over registry-textual matching. */
|
||||
const CBMResolvedCall *lsp =
|
||||
cbm_lsp_resolution_index_find(lsp_idx, lsp_calls, call, ctx->lsp_confidence_floor);
|
||||
bool lsp_target_unindexed = false;
|
||||
if (lsp) {
|
||||
const cbm_gbuf_node_t *target_node = calls_lsp_target_node(ctx, lsp->callee_qn);
|
||||
if (target_node && source_node->id != target_node->id) {
|
||||
@@ -314,6 +315,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
}
|
||||
return SKIP_ONE;
|
||||
}
|
||||
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
|
||||
const char *handler_ref = NULL;
|
||||
const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref);
|
||||
if (route_path) {
|
||||
handle_route_registration(ctx, call, source_node, route_path, handler_ref,
|
||||
module_qn, imp_keys, imp_vals, imp_count);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
lsp_target_unindexed = true;
|
||||
}
|
||||
|
||||
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
|
||||
@@ -334,6 +345,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
cbm_pipeline_try_field_type_hint(ctx->registry, ctx->gbuf, &res, call->callee_name,
|
||||
source_node->id);
|
||||
|
||||
if (lsp_target_unindexed && cbm_registry_strategy_is_weak_short_name(res.strategy) &&
|
||||
!cbm_registry_is_import_reachable(res.qualified_name, imp_vals, imp_count)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Perl call-graph noise guard (#476). Perl has no LSP resolver, so the
|
||||
* generic registry chain is the only resolver; for builtins (push/shift/
|
||||
* keys/...) and method calls ($obj->m with an unresolved receiver), a *weak*
|
||||
|
||||
@@ -289,6 +289,10 @@ bool cbm_registry_exists(const cbm_registry_t *r, const char *qn);
|
||||
* the name. Perl-scoped: callers gate on the file language. */
|
||||
bool cbm_perl_is_builtin(const char *name);
|
||||
|
||||
/* True for registry strategies that are only weak short-name guesses. Strong
|
||||
* same-module and import-map matches return false. */
|
||||
bool cbm_registry_strategy_is_weak_short_name(const char *strategy);
|
||||
|
||||
/* Decide whether a resolved Perl call edge is generic-resolver noise to drop
|
||||
* (#476): true only for Perl, only for a builtin/method call, and only when the
|
||||
* match used a weak short-name strategy — high-confidence same_module/import_map
|
||||
|
||||
+12
-2
@@ -393,13 +393,23 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
|
||||
if (!strategy || !strategy[0]) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(strategy, "same_module") == 0 || strcmp(strategy, "import_map") == 0 ||
|
||||
strcmp(strategy, "import_map_suffix") == 0) {
|
||||
if (!cbm_registry_strategy_is_weak_short_name(strategy)) {
|
||||
return false; /* high-confidence import/same-module match — keep the genuine edge */
|
||||
}
|
||||
return true; /* weak short-name match (suffix_match / unique_name / …) → drop */
|
||||
}
|
||||
|
||||
bool cbm_registry_strategy_is_weak_short_name(const char *strategy) {
|
||||
if (!strategy || !strategy[0]) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(strategy, "same_module") == 0 || strcmp(strategy, "import_map") == 0 ||
|
||||
strcmp(strategy, "import_map_suffix") == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── Lifecycle ──────────────────────────────────────────────────── */
|
||||
|
||||
cbm_registry_t *cbm_registry_new(void) {
|
||||
|
||||
@@ -7956,6 +7956,66 @@ TEST(pipeline_python_super_init_external_lsp_suppresses_suffix_fallback) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_external_lsp_target_suppresses_suffix_fallback) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/proj");
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(gb);
|
||||
ASSERT_NOT_NULL(reg);
|
||||
|
||||
int64_t source_id =
|
||||
cbm_gbuf_upsert_node(gb, "Function", "caller", "proj.app.caller", "app.py", 1, 10,
|
||||
"{}");
|
||||
int64_t suffix_target_id = cbm_gbuf_upsert_node(
|
||||
gb, "Method", "get", "proj.fastapi.routing.APIRouter.get", "routing.py", 1, 10, "{}");
|
||||
int64_t second_suffix_target_id = cbm_gbuf_upsert_node(
|
||||
gb, "Method", "get", "proj.other.Mapping.get", "other.py", 1, 10, "{}");
|
||||
ASSERT_GT(source_id, 0);
|
||||
ASSERT_GT(suffix_target_id, 0);
|
||||
ASSERT_GT(second_suffix_target_id, 0);
|
||||
cbm_registry_add(reg, "get", "proj.fastapi.routing.APIRouter.get", "Method");
|
||||
cbm_registry_add(reg, "get", "proj.other.Mapping.get", "Method");
|
||||
|
||||
CBMFileResult result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
cbm_arena_init(&result.arena);
|
||||
CBMCall call = {.callee_name = "scope.get",
|
||||
.enclosing_func_qn = "proj.app.caller",
|
||||
.start_line = 2};
|
||||
cbm_calls_push(&result.calls, &result.arena, call);
|
||||
CBMResolvedCall resolved = {.caller_qn = "proj.app.caller",
|
||||
.callee_qn = "external.collections.Mapping.get",
|
||||
.strategy = "lsp_external_method",
|
||||
.confidence = 0.95f};
|
||||
cbm_resolvedcall_push(&result.resolved_calls, &result.arena, resolved);
|
||||
CBMFileResult *result_cache[1] = {&result};
|
||||
|
||||
atomic_int cancelled;
|
||||
atomic_init(&cancelled, 0);
|
||||
cbm_pipeline_ctx_t ctx = {.project_name = "proj",
|
||||
.repo_path = "/tmp/proj",
|
||||
.gbuf = gb,
|
||||
.registry = reg,
|
||||
.cancelled = &cancelled,
|
||||
.mode = CBM_MODE_FAST,
|
||||
.result_cache = result_cache};
|
||||
cbm_file_info_t files[1] = {{.path = "/tmp/proj/app.py",
|
||||
.rel_path = "app.py",
|
||||
.language = CBM_LANG_PYTHON}};
|
||||
|
||||
ASSERT_EQ(cbm_pipeline_pass_calls(&ctx, files, 1), 0);
|
||||
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
ASSERT_EQ(cbm_gbuf_find_edges_by_source_type(gb, source_id, "CALLS", &edges, &edge_count),
|
||||
0);
|
||||
ASSERT_EQ(edge_count, 0);
|
||||
|
||||
cbm_arena_destroy(&result.arena);
|
||||
cbm_registry_free(reg);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(registry_fuzzy_confidence_single) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
|
||||
@@ -15198,6 +15258,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(registry_confidence_unique_name);
|
||||
RUN_TEST(registry_confidence_suffix_match);
|
||||
RUN_TEST(pipeline_python_super_init_external_lsp_suppresses_suffix_fallback);
|
||||
RUN_TEST(pipeline_external_lsp_target_suppresses_suffix_fallback);
|
||||
RUN_TEST(registry_fuzzy_confidence_single);
|
||||
RUN_TEST(registry_fuzzy_confidence_distance);
|
||||
RUN_TEST(registry_negative_import_rejects);
|
||||
|
||||
Reference in New Issue
Block a user