From f4efd0b86844bf2ac965ba9ec30c8805cc774e40 Mon Sep 17 00:00:00 2001 From: BobbieBarker <4307099+BobbieBarker@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:50:28 -0700 Subject: [PATCH 1/5] fix(registry): score a candidate on the extractor's test verdict, not its name cbm_registry_add discarded the caller's knowledge of whether a symbol is test code and recovered it from the qualified name: is_test_qn scanned the QN for "test", "Test", "mock", "stub", "spec", "fake" or "Fixture" anywhere in the string. REG_TEST_PENALTY then keeps non-test candidates ahead of test ones, so that scan decides which definition an unresolved call resolves to. An unanchored substring match over a path is wrong in both directions, and the false positives are not exotic. On one 970-file Elixir tree the scan calls 794 of 20,882 Function and Method nodes test code. 516 of those match on "spec", and 268 match ONLY because "inspect" contains it: forge_symphony.pipeline.impl .inspect_optional, .pr_tracker.continuation_intent.inspect_record and every other symbol under an operator_inspect module is ordinary production code that was being ranked below real test code. "latest" and "contest" match "test" the same way. In the other direction a test helper under a directory that spells none of those words is scored as production. The extractor already knows. CBMDefinition.is_test is set from the file's test-ness (cbm_is_test_file) OR the definition's own attributes, and it is the value the store filters on. cbm_registry_add now takes it and stores it beside the QN, and is_test_qn is deleted. Three things follow from the verdict no longer being recomputable: candidate_score loses its tri-state `int is_test` and its `-1: ask` fallback, because there is nothing left to ask. qn_test_flags stops returning NULL. Both filtered resolve paths have to carry the verdicts with the candidates. resolve_multi_with_imports already copied a parallel array but guarded it on `flags ?`; filter_import_reachable carried none at all, so the fuzzy path passed NULL whenever it had filtered and silently fell back to the QN scan. index_under_name drops an entry whose verdict it cannot store, rather than leaving the flag array short. A missing verdict scores as non-test, which would let an unranked candidate outrank real code; one missing candidate is the smaller error. pipeline_incremental.c seeds the registry from persisted nodes and has no CBMDefinition, so it reads the same flag out of properties_json through cbm_node_is_test, exported from pass_tests.c. Its comment requires the incremental registry to mirror the full-index one exactly, and both now answer from the same value. Measured by indexing four corpora with a binary built from this commit's parent and with this one, and diffing the `edges` rows keyed on (source QN, target QN, type): corpus moved retargeted lost (resolve) lost (similarity) Elixir 970-file 920 919 1 0 ripgrep 14.1.1 353 193 30 130 tokio 1.40.0 1,548 1,426 86 36 rust-analyzer 5,160 4,817 219 124 Almost every moved edge is the same call site landing on a different target. 336 resolution edges are lost across the four, where the new winner is refused by a check the old one passed. The similarity column is SEMANTICALLY_RELATED, which the post-passes rebuild wholesale over a drifting corpus and which moves on its own. This is a behaviour change across every indexed project, not a signature change, and the gating suite does not see it: 8,236 tests pass with the scan and without it. REG_TEST_PENALTY had no direct coverage, because none of the 95 cbm_registry_add call sites in the suite registered a symbol the scan would flag. tests/test_registry.c: three tests. registry_prefers_non_test_over_test_ candidate pins the penalty itself. registry_test_verdict_comes_from_the_caller _not_the_qn pins where the verdict comes from, using proj.latest.parse (production code the scan reads as a test) against proj.harness.parse (a test the scan reads as production); it fails with the scan restored and the signature kept. registry_sole_test_candidate_still_resolves pins that the penalty orders candidates without vetoing them. Signed-off-by: Chad <4307099+BobbieBarker@users.noreply.github.com> --- src/pipeline/pass_definitions.c | 2 +- src/pipeline/pass_parallel.c | 2 +- src/pipeline/pass_tests.c | 6 +- src/pipeline/pipeline.h | 5 +- src/pipeline/pipeline_incremental.c | 2 +- src/pipeline/pipeline_internal.h | 6 ++ src/pipeline/registry.c | 134 ++++++++---------------- tests/test_pipeline.c | 78 +++++++------- tests/test_registry.c | 157 ++++++++++++++++++---------- 9 files changed, 199 insertions(+), 193 deletions(-) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 7c7ed68847..c6bb872c46 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -335,7 +335,7 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const * (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed * through the same predicate, so the three registries cannot diverge. */ if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { - cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label, def->is_test); } char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__"); const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index e85ad015f1..7a760fdcb4 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1588,7 +1588,7 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d /* Registry membership is defined ONCE by cbm_label_is_registry_symbol * (helpers.c) — see pass_definitions.c for the per-label rationale. */ if (cbm_label_is_registry_symbol(def->label)) { - cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label, def->is_test); (*reg_entries)++; } const cbm_gbuf_node_t *def_node = cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name); diff --git a/src/pipeline/pass_tests.c b/src/pipeline/pass_tests.c index 9cade02a0d..65c099bf08 100644 --- a/src/pipeline/pass_tests.c +++ b/src/pipeline/pass_tests.c @@ -44,7 +44,7 @@ static const char *itoa_log(int val) { } /* Check if a node has is_test:true in its properties JSON. */ -static bool node_is_test(const cbm_gbuf_node_t *n) { +bool cbm_node_is_test(const cbm_gbuf_node_t *n) { if (!n || !n->properties_json) { return false; } @@ -227,13 +227,13 @@ static int create_tests_edges(cbm_pipeline_ctx_t *ctx) { } bool src_is_test = - node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); + cbm_node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); if (!src_is_test) { continue; } bool tgt_is_test = - node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); + cbm_node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); if (tgt_is_test) { continue; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index c0fddc7e77..9d47e826fc 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -229,8 +229,11 @@ cbm_registry_t *cbm_registry_new(void); void cbm_registry_free(cbm_registry_t *r); /* Register a function/method/class. All strings are copied. */ +/* `is_test` is the extractor's verdict for this symbol (CBMDefinition.is_test: + * the file's test-ness OR the definition's own test attributes). The registry + * stores it and scores with it; it does not derive test-ness from the QN. */ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label); + const char *label, bool is_test); /* Resolve a callee name using prioritized strategies. * import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL. diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 154518658d..cc90c31941 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1041,7 +1041,7 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { if (!incr_label_is_registry_symbol(node->label)) { return; } - cbm_registry_add(r, node->name, node->qualified_name, node->label); + cbm_registry_add(r, node->name, node->qualified_name, node->label, cbm_node_is_test(node)); } static void free_incremental_result_cache(CBMFileResult **cache, int count) { diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index e7782adeff..6c7ac96e92 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -166,6 +166,12 @@ typedef struct { typedef bool (*cbm_result_want_fn)(const CBMFileResult *header); CBMFileResult *cbm_pipeline_result_acquire(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache, int i, cbm_result_want_fn want, bool *loaded); +/* True when a graph-buffer node carries is_test:true in its properties JSON. + * The incremental registry seed has only the persisted node, not the + * CBMDefinition the definition passes hand to cbm_registry_add, and both must + * give the registry the same verdict for the same symbol. */ +bool cbm_node_is_test(const cbm_gbuf_node_t *n); + void cbm_pipeline_result_release(CBMFileResult *r, bool loaded); /* Log the store counters, close and delete the store, drop the latch. */ diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index bdf6f5944d..c0db4d71fb 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -28,6 +28,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "cbm.h" /* cbm_label_is_relation — the resolve-time relation veto */ #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.h" +#include "foundation/log.h" #include "foundation/dyn_array.h" #include "foundation/platform.h" @@ -72,16 +73,16 @@ const char *cbm_confidence_band(double score) { /* ── Internal types ──────────────────────────────────────────────── */ -/* Array of QN strings for byName index, with the test/mock verdict of each - * name cached beside it: the scorer asked is_test_qn for every candidate of - * every unresolved call, and the answer depends only on the name, which never - * changes once registered (waste sanitizer scaling lane, 2026-09-17). Field - * order and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */ +/* Array of QN strings for byName index, with each entry's test verdict beside + * it. The verdict is the extractor's, handed to cbm_registry_add by the caller + * that already computed it from the file path and the definition's attributes; + * it is stored rather than derived, so every entry always has one. Field order + * and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */ typedef struct { char **items; int count; int cap; - uint8_t *is_test; /* is_test_qn(items[i]), one byte per entry */ + uint8_t *is_test; /* the caller's verdict for items[i], one byte per entry */ int is_test_cap; } qn_array_t; @@ -151,78 +152,19 @@ static int common_prefix_len(const char *a, const char *b) { enum { REG_TEST_PENALTY = 1000 }; -/* Check if a qualified name looks like a test/mock path: does it contain any of - * Test, test, Mock, mock, Stub, stub, Fake, fake, Fixture, spec? One pass that - * dispatches on the first character instead of ten strstr scans of the whole - * name (this ran for every candidate of every unresolved call: the scaling - * lane's x4.0, waste sanitizer 2026-09-17). */ -static bool is_test_qn(const char *qn) { - if (!qn) { - return false; - } - for (const char *p = qn; *p; p++) { - switch (*p) { - case 'T': - if (strncmp(p, "Test", 4) == 0) { - return true; - } - break; - case 't': - if (strncmp(p, "test", 4) == 0) { - return true; - } - break; - case 'M': - if (strncmp(p, "Mock", 4) == 0) { - return true; - } - break; - case 'm': - if (strncmp(p, "mock", 4) == 0) { - return true; - } - break; - case 'S': - if (strncmp(p, "Stub", 4) == 0) { - return true; - } - break; - case 's': - if (strncmp(p, "stub", 4) == 0 || strncmp(p, "spec", 4) == 0) { - return true; - } - break; - case 'F': - if (strncmp(p, "Fake", 4) == 0 || strncmp(p, "Fixture", 7) == 0) { - return true; - } - break; - case 'f': - if (strncmp(p, "fake", 4) == 0) { - return true; - } - break; - default: - break; - } - } - return false; -} - -/* The cached test verdicts, only when they cover every entry: a failed growth - * leaves the cache short, and then the scorer asks per candidate as before. */ +/* The stored test verdicts. index_under_name drops an entry it cannot record a + * verdict for, so this covers every entry of a non-empty bucket. */ static const uint8_t *qn_test_flags(const qn_array_t *arr) { - return arr->is_test && arr->is_test_cap >= arr->count ? arr->is_test : NULL; + return arr->is_test; } /* Score a candidate for tiebreaking. Higher = better. * Layer 1: Non-test code preferred over test code (+1000) * Layer 2: Namespace proximity via common prefix length (+plen) - * `is_test` is the cached verdict when the caller has one (-1: ask). */ -static int candidate_score(const char *candidate_qn, const char *module_qn, int is_test) { + * `is_test` is the verdict stored for this candidate at registration. */ +static int candidate_score(const char *candidate_qn, const char *module_qn, bool is_test) { int score = 0; - bool test = is_test >= 0 ? is_test != 0 : is_test_qn(candidate_qn); - if (!test) { + if (!is_test) { score += REG_TEST_PENALTY; } score += common_prefix_len(candidate_qn, module_qn); @@ -268,8 +210,7 @@ static const char *best_by_import_distance(const char **candidates, const uint8_ const char *best = NULL; int best_score = CBM_NOT_FOUND; for (int i = 0; i < count; i++) { - int score = - candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1); + int score = candidate_score(candidates[i], module_qn, is_test_flags[i] != 0); if (score > best_score || (score == best_score && best && candidate_outranks_on_tie(candidates[i], best))) { best_score = score; @@ -863,7 +804,8 @@ void cbm_registry_free(cbm_registry_t *r) { * * No array dedup needed: cbm_registry_add's exact-map check guarantees the QN * is new, and it calls this at most once per distinct key. */ -static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn) { +static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn, + bool is_test) { qn_array_t *arr = cbm_ht_get(r->by_name, key); if (!arr) { arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t)); @@ -872,7 +814,7 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own int before = arr->count; cbm_da_push(arr, (char *)owned_qn); if (arr->count == before) { - return; /* the name could not be recorded: no verdict to cache */ + return; /* the name could not be recorded: no verdict to store */ } if (arr->count > arr->is_test_cap) { int want = arr->cap > 0 ? arr->cap : arr->count; @@ -883,13 +825,19 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own arr->is_test_cap = want; } } - if (arr->count <= arr->is_test_cap) { - arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0; + if (arr->count > arr->is_test_cap) { + /* The verdict can no longer be recomputed from the QN, so an entry + * without one would be scored as non-test and outrank real code. Drop + * it instead: one missing candidate beats a wrong winner. */ + arr->count = before; + cbm_log_warn("registry.verdict_alloc_failed", "key", key); + return; } + arr->is_test[arr->count - SKIP_ONE] = is_test ? 1 : 0; } void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label) { + const char *label, bool is_test) { if (!r || !qualified_name || !label) { return; } @@ -945,14 +893,14 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified * `name` is NULL or empty only for callers that have no symbol name to * give; those have the derived key and nothing else. */ const char *derived = simple_name(qualified_name); - index_under_name(r, derived, owned_qn); + index_under_name(r, derived, owned_qn, is_test); /* '#' is a QN fence, and extract_defs.c's rust_cfg_qualified_name is the * only thing in the tree that mints one today. A grammar that starts * minting a '#' opts into this second key by doing so, whatever it means by * the fence: its symbols become reachable under the passed name as well, * and they share that name's bucket with everything else filed under it. */ if (name && name[0] && strchr(derived, '#') && strcmp(name, derived) != 0) { - index_under_name(r, name, owned_qn); + index_under_name(r, name, owned_qn, is_test); } } @@ -1111,9 +1059,7 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const int fcount = 0; for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) { if (is_import_reachable(arr->items[i], import_vals, import_count)) { - if (flags) { - filtered_test[fcount] = flags[i]; - } + filtered_test[fcount] = flags[i]; filtered[fcount] = arr->items[i]; fcount++; } @@ -1123,8 +1069,7 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count}; } if (fcount > SKIP_ONE) { - const char *best = - best_by_import_distance(filtered, flags ? filtered_test : NULL, fcount, module_qn); + const char *best = best_by_import_distance(filtered, filtered_test, fcount, module_qn); if (best) { double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount); return (cbm_resolution_t){best, "suffix_match", conf, fcount}; @@ -1454,11 +1399,15 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha /* ── Fuzzy Resolve ──────────────────────────────────────────────── */ /* Filter candidates by import reachability. Returns count of reachable. */ -static int filter_import_reachable(const char **candidates, int count, const char **import_vals, - int import_count, const char **out, int max_out) { +/* Keeps `out_flags` in step with `out`: the verdicts are stored per entry and + * cannot be recomputed from the QN, so a filtered view has to carry its own. */ +static int filter_import_reachable(const char **candidates, const uint8_t *flags, int count, + const char **import_vals, int import_count, const char **out, + uint8_t *out_flags, int max_out) { int n = 0; for (int i = 0; i < count && n < max_out; i++) { if (is_import_reachable(candidates[i], import_vals, import_count)) { + out_flags[n] = flags[i]; out[n++] = candidates[i]; } } @@ -1498,13 +1447,17 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha /* Multiple candidates: filter by import reachability */ const char *filtered[CBM_SZ_256]; + uint8_t filtered_flags[CBM_SZ_256]; int fcount = arr->count; const char **fptr = (const char **)arr->items; + const uint8_t *fflags = qn_test_flags(arr); if (have_imports) { - fcount = filter_import_reachable((const char **)arr->items, arr->count, import_map_vals, - import_map_count, filtered, CBM_SZ_256); + fcount = filter_import_reachable((const char **)arr->items, qn_test_flags(arr), arr->count, + import_map_vals, import_map_count, filtered, + filtered_flags, CBM_SZ_256); fptr = filtered; + fflags = filtered_flags; } if (fcount == 0) { @@ -1524,8 +1477,7 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha {fptr[0], "fuzzy", candidate_count_penalty(CONF_FUZZY_SINGLE, arr->count), arr->count}, true}; } - const char *best = best_by_import_distance( - fptr, fptr == (const char **)arr->items ? qn_test_flags(arr) : NULL, fcount, module_qn); + const char *best = best_by_import_distance(fptr, fflags, fcount, module_qn); if (!best) { return no_match; } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 568740f170..f5a1ddd4e2 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10930,8 +10930,8 @@ TEST(helm_parse_chart_no_deps_issue338) { TEST(registry_resolve_single_candidate) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); - cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function"); + cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); + cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false); /* Normal resolve unique name */ cbm_resolution_t r = cbm_registry_resolve(reg, "CreateOrder", "svcC.caller", NULL, NULL, 0); @@ -10949,7 +10949,7 @@ TEST(registry_resolve_single_candidate) { TEST(registry_fuzzy_nonexistent) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); + cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "NonExistent", "svcC.caller", NULL, NULL, 0); @@ -10961,8 +10961,8 @@ TEST(registry_fuzzy_nonexistent) { TEST(registry_fuzzy_multiple_best_by_distance) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function"); - cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function"); + cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function", false); + cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function", false); /* Caller in svcA → prefer svcA */ cbm_fuzzy_result_t fr = @@ -10981,7 +10981,7 @@ TEST(registry_fuzzy_multiple_best_by_distance) { TEST(registry_fuzzy_simple_name_extraction) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function"); + cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function", false); /* Deeply qualified name → extract "DoWork" */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "some.deep.module.DoWork", @@ -11006,8 +11006,8 @@ TEST(registry_fuzzy_empty) { TEST(registry_exists) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function"); - cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method"); + cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method", false); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Foo")); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Bar")); @@ -11020,7 +11020,7 @@ TEST(registry_exists) { TEST(registry_confidence_import_map) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -11035,7 +11035,7 @@ TEST(registry_confidence_import_map) { TEST(registry_confidence_import_map_suffix) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -11050,7 +11050,7 @@ TEST(registry_confidence_import_map_suffix) { TEST(registry_confidence_same_module) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); @@ -11063,7 +11063,7 @@ TEST(registry_confidence_same_module) { TEST(registry_confidence_unique_name) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); @@ -11076,8 +11076,8 @@ TEST(registry_confidence_unique_name) { TEST(registry_confidence_suffix_match) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); - cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); + cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.svcA.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.svcA.Process"); @@ -11093,7 +11093,7 @@ TEST(registry_confidence_suffix_match) { * wrong target — and with one candidate it won the top name-only confidence. */ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); + cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); @@ -11107,8 +11107,8 @@ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) { * the final name and import distance picks the winner. */ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); - cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable"); + cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false); + cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable", false); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); @@ -11122,7 +11122,7 @@ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) { * so Calendar really is in the receiver chain. */ TEST(registry_receiver_chain_keeps_project_extension_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method"); + cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC", "HomeboxUI.Stats", NULL, NULL, 0); @@ -11142,7 +11142,7 @@ TEST(registry_receiver_chain_keeps_project_extension_issue1893) { TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "putList", "org.elasticsearch.common.settings.Settings.Builder.putList", - "Method"); + "Method", false); cbm_resolution_t r = cbm_registry_resolve( reg, "Settings.builder().putList", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); @@ -11153,7 +11153,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { * .toBuilder().setWarningsHandler -> RequestOptions.Builder.setWarningsHandler. */ cbm_registry_add(reg, "setWarningsHandler", "org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler", - "Method"); + "Method", false); r = cbm_registry_resolve(reg, "RequestOptions.DEFAULT.toBuilder().setWarningsHandler", "org.elasticsearch.client.Rest", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, @@ -11169,7 +11169,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "getEncoder", "org.elasticsearch.index.codec.DocOffsetsCodec.getEncoder", - "Method"); + "Method", false); cbm_resolution_t r = cbm_registry_resolve( reg, "Base64.getEncoder", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); @@ -11179,7 +11179,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { * org.elasticsearch.common.Math.toIntExact through "Math" appearing as an * ancestry segment ONLY IF that segment really is the type — here it is, and * that is the correct outcome (the project's own Math.toIntExact). */ - cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method"); + cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method", false); r = cbm_registry_resolve(reg, "Math.toIntExact", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.Math.toIntExact"); @@ -11192,7 +11192,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { * must not look at it, or every ordinary vm.load style call would be refused. */ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method"); + cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method", false); cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load"); @@ -11205,7 +11205,7 @@ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { /* An unqualified callee has no chain at all and must pass through unchanged. */ TEST(registry_receiver_chain_ignores_bare_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function"); + cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper"); @@ -11217,7 +11217,7 @@ TEST(registry_receiver_chain_ignores_bare_name_issue1893) { TEST(registry_fuzzy_confidence_single) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); @@ -11231,8 +11231,8 @@ TEST(registry_fuzzy_confidence_single) { TEST(registry_fuzzy_confidence_distance) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); - cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); + cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); @@ -11246,8 +11246,8 @@ TEST(registry_fuzzy_confidence_distance) { TEST(registry_negative_import_rejects) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Process", "proj.billing.Process", "Function"); - cbm_registry_add(reg, "Process", "proj.handler.Process", "Function"); + cbm_registry_add(reg, "Process", "proj.billing.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.handler.Process", "Function", false); /* Import only handler's module → should prefer handler */ const char *keys[] = {"handler"}; @@ -11261,7 +11261,7 @@ TEST(registry_negative_import_rejects) { TEST(registry_fuzzy_import_penalty) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false); /* Has imports but billing not imported → confidence halved */ const char *keys[] = {"other"}; @@ -11278,7 +11278,7 @@ TEST(registry_fuzzy_import_penalty) { TEST(registry_fuzzy_no_import_map_passthrough) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false); /* NULL import map → no penalty, full fuzzy confidence */ cbm_fuzzy_result_t fr = @@ -11292,10 +11292,10 @@ TEST(registry_fuzzy_no_import_map_passthrough) { TEST(registry_find_by_name) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); - cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); - cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); - cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); + cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false); /* FindByName returns all entries for "Foo" */ const char **foos = NULL; @@ -12628,10 +12628,10 @@ TEST(registry_is_import_reachable) { /* Port of FindEndingWith portion from Go TestFunctionRegistry in pipeline_test.go */ TEST(registry_find_ending_with) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); - cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); - cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); - cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); + cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false); /* FindEndingWith "DataProcessor.transform" → 1 match */ const char **matches = NULL; diff --git a/tests/test_registry.c b/tests/test_registry.c index f77b1460bb..beb125024a 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -191,7 +191,7 @@ TEST(registry_free_null) { TEST(registry_add_and_exists) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "main", "proj.cmd.main", "Function"); + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); ASSERT_EQ(cbm_registry_size(r), 1); ASSERT_TRUE(cbm_registry_exists(r, "proj.cmd.main")); ASSERT_FALSE(cbm_registry_exists(r, "proj.cmd.other")); @@ -199,10 +199,52 @@ TEST(registry_add_and_exists) { PASS(); } +/* REG_TEST_PENALTY had no direct coverage. Of the registry call sites in this + * suite, none registered a symbol the scorer would treat as a test, so the + * layer that keeps production code ahead of test code was never exercised. + * These three pin it, and pin where the verdict comes from. */ +TEST(registry_prefers_non_test_over_test_candidate) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "helper", "proj.app.helper", "Function", false); + cbm_registry_add(r, "helper", "proj.harness.helper", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.app.helper"); + cbm_registry_free(r); + PASS(); +} + +/* The verdict is the caller's, not the QN's spelling. "latest" contains "test", + * so the scan this replaces read ordinary production code as a test and pushed + * it below a real one; "harness" spells nothing, so the same scan read a test + * as production. Both verdicts here are the extractor's, and both are right. */ +TEST(registry_test_verdict_comes_from_the_caller_not_the_qn) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "parse", "proj.latest.parse", "Function", false); + cbm_registry_add(r, "parse", "proj.harness.parse", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "parse", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.latest.parse"); + cbm_registry_free(r); + PASS(); +} + +/* A bucket of one is returned whatever its verdict: the penalty orders + * candidates, it does not veto them. */ +TEST(registry_sole_test_candidate_still_resolves) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "only", "proj.harness.only", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "only", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.harness.only"); + cbm_registry_free(r); + PASS(); +} + TEST(registry_label_of) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class"); - cbm_registry_add(r, "bar", "proj.pkg.bar", "Function"); + cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class", false); + cbm_registry_add(r, "bar", "proj.pkg.bar", "Function", false); ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.Foo"), "Class"); ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.bar"), "Function"); @@ -214,9 +256,9 @@ TEST(registry_label_of) { TEST(registry_find_by_name) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "main", "proj.cmd.main", "Function"); - cbm_registry_add(r, "main", "proj.srv.main", "Function"); - cbm_registry_add(r, "helper", "proj.util.helper", "Function"); + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); + cbm_registry_add(r, "main", "proj.srv.main", "Function", false); + cbm_registry_add(r, "helper", "proj.util.helper", "Function", false); const char **out = NULL; int count = 0; @@ -235,8 +277,8 @@ TEST(registry_find_by_name) { TEST(registry_no_duplicates) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "main", "proj.cmd.main", "Function"); - cbm_registry_add(r, "main", "proj.cmd.main", "Function"); /* duplicate */ + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); /* duplicate */ ASSERT_EQ(cbm_registry_size(r), 1); const char **out = NULL; @@ -252,7 +294,7 @@ TEST(registry_no_duplicates) { TEST(resolve_same_module) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function"); + cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function", false); /* Call "helper" from the same module → should resolve */ cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.pkg.service", NULL, NULL, 0); @@ -270,9 +312,9 @@ TEST(resolve_same_module) { * Foo::Bar::sub()) where the same sub name exists in multiple packages. */ TEST(resolve_qualified_disambiguates_same_name) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function"); - cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function"); - cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function"); + cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function", false); + cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function", false); + cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function", false); /* Each fully-qualified call routes to its own package. */ cbm_resolution_t a = @@ -313,8 +355,8 @@ TEST(resolve_qualified_disambiguates_same_name) { * pick arbitrarily under the high-confidence qualified_suffix strategy. */ TEST(resolve_qualified_ambiguous_tail_falls_through) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function"); - cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function"); + cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function", false); + cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function", false); /* "Foo::Bar::run" tail matches BOTH candidates → not unique → fall through. */ cbm_resolution_t res = @@ -332,7 +374,7 @@ TEST(resolve_qualified_ambiguous_tail_falls_through) { * `add` callee could ever reach it. */ TEST(registry_indexes_by_passed_name_not_qn_tail) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function"); + cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function", false); cbm_resolution_t res = cbm_registry_resolve(r, "add", "proj.lib.caller", NULL, NULL, 0); ASSERT_STR_EQ(res.qualified_name, "proj.lib.add#cfg(test)"); @@ -357,9 +399,9 @@ TEST(registry_indexes_by_passed_name_not_qn_tail) { * lookup a bare module reference needs. */ TEST(registry_indexes_a_dotted_name_under_its_tail_too) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web", - "Class"); - cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module"); + cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web", "Class", + false); + cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module", false); cbm_resolution_t tail = cbm_registry_resolve(r, "web", "proj.main", NULL, NULL, 0); ASSERT_STR_EQ(tail.qualified_name, "proj.main.resource.aws_instance.web"); @@ -377,7 +419,7 @@ TEST(registry_indexes_a_dotted_name_under_its_tail_too) { TEST(resolve_import_map) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function"); + cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function", false); /* Import map: "worker" → "proj.pkg.worker" */ const char *keys[] = {"worker"}; @@ -400,9 +442,9 @@ TEST(resolve_import_map) { * file. Regression for the @/lib/auth-style import case. */ TEST(resolve_import_map_bare_function) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function"); + cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function", false); /* Same name in another module — without the fix this is what gets picked. */ - cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function"); + cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function", false); const char *keys[] = {"requireAdmin"}; const char *vals[] = {"proj.lib.authorization"}; @@ -423,7 +465,7 @@ TEST(resolve_import_map_bare_function) { * Regression for #875. */ TEST(resolve_import_map_bare_alias) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function"); + cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function", false); /* Import map: alias "_scan_bash" → FULL SYMBOL QN (not the module). */ const char *keys[] = {"_scan_bash"}; const char *vals[] = {"proj.security_scan.scan_bash"}; @@ -440,10 +482,10 @@ TEST(resolve_import_map_bare_alias) { * already on main via #875/#979; this locks the Yui G1 shape. */ TEST(resolve_import_map_aliased_from_import) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function"); + cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function", false); /* Alias ghost must not win if somehow registered. */ cbm_registry_add(r, "bridge_execute", "proj.services.satori_bridge.gate.bridge_execute", - "Function"); + "Function", false); const char *keys[] = {"bridge_execute"}; const char *vals[] = {"proj.services.satori_bridge.gate.execute"}; @@ -459,7 +501,7 @@ TEST(resolve_import_map_aliased_from_import) { TEST(resolve_unique_name) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function"); + cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function", false); /* Call "UniqueFunc" — only one candidate project-wide */ cbm_resolution_t res = @@ -473,7 +515,7 @@ TEST(resolve_unique_name) { TEST(resolve_unresolved) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "foo", "proj.pkg.foo", "Function"); + cbm_registry_add(r, "foo", "proj.pkg.foo", "Function", false); /* Call "nonexistent" — not in registry */ cbm_resolution_t res = cbm_registry_resolve(r, "nonexistent", "proj.other", NULL, NULL, 0); @@ -490,7 +532,7 @@ TEST(resolve_many_nodes) { char name[32], qn[64]; snprintf(name, sizeof(name), "func_%d", i); snprintf(qn, sizeof(qn), "proj.pkg.func_%d", i); - cbm_registry_add(r, name, qn, "Function"); + cbm_registry_add(r, name, qn, "Function", false); } ASSERT_EQ(cbm_registry_size(r), 500); @@ -528,8 +570,8 @@ TEST(confidence_band_speculative) { TEST(resolve_suffix_match) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.svcA.Process", "Function"); - cbm_registry_add(r, "Process", "proj.svcB.Process", "Function"); + cbm_registry_add(r, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false); /* Caller in svcA — should prefer svcA via import distance */ cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.svcA.caller", NULL, NULL, 0); @@ -552,7 +594,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) { for (int i = 0; i < 300; i++) { char qn[64]; snprintf(qn, sizeof(qn), "proj.mod%d.flags", i); - cbm_registry_add(r, "flags", qn, "Variable"); + cbm_registry_add(r, "flags", qn, "Variable", false); } cbm_resolution_t res = cbm_registry_resolve(r, "flags", "proj.other.caller", NULL, NULL, 0); ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); @@ -570,7 +612,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) { TEST(resolve_import_map_suffix) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function"); + cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -592,7 +634,7 @@ TEST(resolve_is_import_reachable) { /* Test import reachability through unique_name confidence penalty. * is_import_reachable is static, so we test it indirectly. */ cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function"); + cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function", false); /* With import covering the module → full confidence */ const char *keys1[] = {"utils"}; @@ -615,7 +657,7 @@ TEST(resolve_is_import_reachable) { TEST(resolve_import_reachable_prefix) { /* "proj.handler.sub.Process" should be reachable via import "proj.handler" */ cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function"); + cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function", false); const char *keys[] = {"handler"}; const char *vals[] = {"proj.handler"}; @@ -669,8 +711,8 @@ TEST(reach_cache_memo_matches_uncached_across_files) { TEST(negative_import_rejects_unimported) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.billing.Process", "Function"); - cbm_registry_add(r, "Process", "proj.handler.Process", "Function"); + cbm_registry_add(r, "Process", "proj.billing.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.handler.Process", "Function", false); /* Import only handler's module — suffix_match should prefer handler */ const char *keys[] = {"handler"}; @@ -686,8 +728,8 @@ TEST(negative_import_rejects_unimported) { TEST(fuzzy_resolve_single_candidate) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); - cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function"); + cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); + cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false); /* FuzzyResolve should find by simple name even with unknown prefix */ cbm_fuzzy_result_t fr = @@ -701,7 +743,7 @@ TEST(fuzzy_resolve_single_candidate) { TEST(fuzzy_resolve_nonexistent) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); + cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "NonExistent", "svcC.caller", NULL, NULL, 0); @@ -713,8 +755,8 @@ TEST(fuzzy_resolve_nonexistent) { TEST(fuzzy_resolve_multiple_best_by_distance) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function"); - cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function"); + cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function", false); + cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function", false); /* Caller in svcA — should prefer svcA */ cbm_fuzzy_result_t fr = @@ -733,7 +775,7 @@ TEST(fuzzy_resolve_multiple_best_by_distance) { TEST(fuzzy_resolve_deep_name_extraction) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function"); + cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function", false); /* Deeply qualified callee — should extract "DoWork" */ cbm_fuzzy_result_t fr = @@ -758,7 +800,7 @@ TEST(fuzzy_resolve_empty_registry) { TEST(fuzzy_resolve_confidence_single) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); @@ -772,8 +814,8 @@ TEST(fuzzy_resolve_confidence_single) { TEST(fuzzy_resolve_confidence_distance) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.svcA.Process", "Function"); - cbm_registry_add(r, "Process", "proj.svcB.Process", "Function"); + cbm_registry_add(r, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); @@ -787,7 +829,7 @@ TEST(fuzzy_resolve_confidence_distance) { TEST(fuzzy_penalty_unreachable_import) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false); /* Import for different module → confidence halved */ const char *keys[] = {"other"}; @@ -804,7 +846,7 @@ TEST(fuzzy_penalty_unreachable_import) { TEST(fuzzy_no_import_map_passthrough) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false); /* nil import map → full confidence */ cbm_fuzzy_result_t fr = @@ -913,8 +955,8 @@ TEST(registry_tie_break_is_independent_of_registration_order) { cbm_registry_t *forward = cbm_registry_new(); cbm_registry_t *backward = cbm_registry_new(); for (int i = 0; i < n; i++) { - cbm_registry_add(forward, "dev_name", cands[i], labels[i]); - cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i]); + cbm_registry_add(forward, "dev_name", cands[i], labels[i], false); + cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i], false); } /* A far-away caller with no imports: every candidate scores the same. */ @@ -932,10 +974,10 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Equal depth: the smaller QN, from either order. */ cbm_registry_t *lex_a = cbm_registry_new(); cbm_registry_t *lex_b = cbm_registry_new(); - cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function"); + cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false); cbm_resolution_t la = cbm_registry_resolve(lex_a, "sg_next", "proj.drivers.scsi.arm_scsi", NULL, NULL, 0); cbm_resolution_t lb = @@ -945,9 +987,9 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Proximity still outranks depth: the sibling wins over a shallower stranger. */ cbm_registry_t *near = cbm_registry_new(); - cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function"); + cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function", false); cbm_registry_add(near, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free", - "Function"); + "Function", false); cbm_resolution_t nr = cbm_registry_resolve(near, "vnic_rq_free", "proj.drivers.scsi.fnic.fnic_res", NULL, NULL, 0); ASSERT_STR_EQ(nr.qualified_name, "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free"); @@ -1209,8 +1251,8 @@ TEST(weak_call_guards_share_one_drop_list) { * Signal.send). Regression guard for #1000. */ TEST(resolve_import_map_alias_with_suffix_hits_method) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable"); - cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method"); + cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable", false); + cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method", false); const char *keys[] = {"user_logged_in"}; const char *vals[] = {"proj.auth.signals.user_logged_in"}; cbm_resolution_t res = @@ -1244,6 +1286,9 @@ SUITE(registry) { RUN_TEST(registry_create_free); RUN_TEST(registry_free_null); RUN_TEST(registry_add_and_exists); + RUN_TEST(registry_prefers_non_test_over_test_candidate); + RUN_TEST(registry_test_verdict_comes_from_the_caller_not_the_qn); + RUN_TEST(registry_sole_test_candidate_still_resolves); RUN_TEST(registry_label_of); RUN_TEST(registry_find_by_name); RUN_TEST(registry_no_duplicates); From 8a07514de070be3a268200b59971c72fe2060afb Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 11 Nov 2026 20:17:58 +0100 Subject: [PATCH 2/5] style(registry): clang-format the declarations the main merge re-signed index_under_name, swift_choose_candidate and the fuzzy path's filter_import_reachable call gained parameters in the merge of main; this re-wraps exactly those lines the way clang-format (LLVM 23) lays them out. No code change. Signed-off-by: Martin Vogel --- src/pipeline/registry.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index fce3c43cce..2b93bc93cd 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1666,8 +1666,8 @@ void cbm_registry_free(cbm_registry_t *r) { * * No array dedup needed: cbm_registry_add's exact-map check guarantees the QN * is new, and it calls this at most once per distinct key. */ -static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn, - bool is_test, CBMLanguage lang) { +static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn, bool is_test, + CBMLanguage lang) { qn_array_t *arr = cbm_ht_get(r->by_name, key); if (!arr) { arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t)); @@ -3055,11 +3055,10 @@ static const char *swift_choose_receiver(const cbm_registry_t *r, const char **m } static const char *swift_choose_candidate(const cbm_registry_t *r, const char **matches, - const uint8_t *match_test, int matched, - const char *name, const CBMCall *call, - const char *module_qn, const char **import_vals, - int import_count, const char **scratch, - uint8_t *scratch_test) { + const uint8_t *match_test, int matched, const char *name, + const CBMCall *call, const char *module_qn, + const char **import_vals, int import_count, + const char **scratch, uint8_t *scratch_test) { const char *chosen = swift_choose_receiver(r, matches, match_test, matched, name, call, module_qn, import_vals, import_count, scratch, scratch_test); @@ -4781,8 +4780,8 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha if (have_imports) { fcount = filter_import_reachable((const char **)arr->items, qn_test_flags(arr), qn_lang_flags(arr), arr->count, import_map_vals, - import_map_count, filtered, filtered_flags, - filtered_langs, CBM_SZ_256); + import_map_count, filtered, filtered_flags, filtered_langs, + CBM_SZ_256); fptr = filtered; fflags = filtered_flags; flangs = filtered_langs; From 6519e1a797a1f6e2bc9001e99f552fcd470c4d1e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 11 Nov 2026 20:28:54 +0100 Subject: [PATCH 3/5] fix(registry): rank test doubles below real code by an anchored path rule The extractor's verdict is the right primary signal, but Go projects keep many test doubles in ordinary, non-test files: generated fake clientsets, mock packages, mock_*.go files. The QN scan this branch removed happened to rank those below real code; the verdict alone does not, so calls from production code started resolving into them (gardener: about 570 call edges newly landed in fake or mock files, 73 of them from production code). cbm_registry_test_double_path is one registry-only predicate: a directory segment named exactly fake, mock, mocks, testing or testutil, or a file named mock_*.go or fake_*.go. It is anchored to whole segments and the file-name prefix, so inspect, latest, contest, fakeroot or mockery never match. All three registry seeders pass the extractor verdict OR this predicate on the path the definition's node stores (pass_definitions.c, pass_parallel.c, and the incremental seed in pipeline_incremental.c), so a full and an incremental index rank alike. register_and_link_def now takes the file's rel path for that, as pass_definitions.c already has it. The rule only orders candidates when a call is resolved. It stays out of cbm_is_test_path and cbm_is_test_file, so the stored is_test property, TESTS edges and importance do not change. Co-authored-by: Chad <4307099+BobbieBarker@users.noreply.github.com> Signed-off-by: Martin Vogel --- src/pipeline/pass_definitions.c | 7 ++++-- src/pipeline/pass_parallel.c | 13 ++++++---- src/pipeline/pipeline.h | 12 +++++++++ src/pipeline/pipeline_incremental.c | 5 +++- src/pipeline/registry.c | 38 +++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 9a212d319e..44678b76c3 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -521,13 +521,16 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const * field resolution), Variable/Field (READS/WRITES resolution), and Table/View * (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed * through the same predicate, so the three registries cannot diverge. - * A generated member is registered by qualified name only, at all three. */ + * A generated member is registered by qualified name only, at all three. + * The registry's test verdict is the extractor's OR the test-double path + * rule (cbm_registry_test_double_path) on the node's file, at all three. */ if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { if (cbm_def_props_generated(def->extra_props)) { cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label); } else { cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, - def->label, lang, module_prefix_len, def->is_test); + def->label, lang, module_prefix_len, + def->is_test || cbm_registry_test_double_path(def_rel)); } if (def->qn_sig_off) { cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index dddc18b80a..4ae2ea88f4 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1816,7 +1816,7 @@ int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, /* `file_node_id` is the defining file's node id (0 when it has none), looked up * once per file by the caller: computing the file QN and finding its node for * every definition was 700 k allocations and lookups on the Go corpus. */ -static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, +static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel, int64_t file_node_id, CBMLanguage lang, size_t module_prefix_len, int *reg_entries) { int edges = 0; @@ -1824,14 +1824,17 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d return 0; } /* Registry membership is defined ONCE by cbm_label_is_registry_symbol - * (helpers.c) — see pass_definitions.c for the per-label rationale, and - * for the generated member registered by qualified name only. */ + * (helpers.c) — see pass_definitions.c for the per-label rationale, for + * the generated member registered by qualified name only, and for the + * test-double path rule, judged on the path the def's node stores. */ if (cbm_label_is_registry_symbol(def->label)) { if (cbm_def_props_generated(def->extra_props)) { cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label); } else { + const char *def_rel = def->file_path ? def->file_path : rel; cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, - def->label, lang, module_prefix_len, def->is_test); + def->label, lang, module_prefix_len, + def->is_test || cbm_registry_test_double_path(def_rel)); } if (def->qn_sig_off) { cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, @@ -2050,7 +2053,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t : 0; for (int d = 0; d < result->defs.count; d++) { defines_edges += - register_and_link_def(ctx, &result->defs.items[d], file_node_id, + register_and_link_def(ctx, &result->defs.items[d], rel, file_node_id, files[i].language, py_prefix_len, ®_entries); } } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 80611ae17d..83907ae4b0 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -290,6 +290,18 @@ void cbm_registry_free(cbm_registry_t *r); void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label, bool is_test); +/* The test-double path rule, for the registry's ranking only: true when the + * repo-relative path of a symbol's defining file has a directory segment named + * exactly `fake`, `mock`, `mocks`, `testing` or `testutil`, or its file name + * matches `mock_*.go` or `fake_*.go`. Go projects keep generated fake + * clientsets and mock packages in ordinary files the extractor rightly does + * not call tests; a call should still prefer real code over them. The three + * registry seeders (pass_definitions.c, pass_parallel.c, + * pipeline_incremental.c) pass the extractor's verdict OR this, so full and + * incremental indexes rank alike. It never decides what a node is: the stored + * is_test property, TESTS edges and importance stay with cbm_is_test_path. */ +bool cbm_registry_test_double_path(const char *rel_path); + /* Swift-only metadata and label-compatible overload selection. The returned * QNs are borrowed from the registry and all belong to one base callable. * -1 means this name has Swift definitions but no compatible overload. */ diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 80e6d906ed..9324314cdf 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1396,8 +1396,11 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { size_t prefix_len = lang == CBM_LANG_PYTHON ? cbm_pipeline_fqn_symbol_prefix_len(seed->project, node->file_path) : 0; + /* The full index's verdict: the extractor's (stored as is_test) OR the + * test-double path rule on the node's file (pass_definitions.c). */ + bool is_test = cbm_node_is_test(node) || cbm_registry_test_double_path(node->file_path); cbm_registry_add_lang_in_module(seed->registry, node->name, node->qualified_name, node->label, - lang, prefix_len, cbm_node_is_test(node)); + lang, prefix_len, is_test); if (node->properties_json) { const char *mask = strstr(node->properties_json, "\"param_default_mask\":\""); const char *count = strstr(node->properties_json, "\"selector_param_count\":"); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 2b93bc93cd..ac1b8ce618 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -205,6 +205,44 @@ static int common_prefix_len(const char *a, const char *b) { enum { REG_TEST_PENALTY = 1000 }; +/* A directory segment that, by name alone, holds test doubles: Go's generated + * fake clientsets and listers (pkg/client/.../fake), mock packages, and the + * testing / testutil helper packages. Whole segments only, compared exactly. */ +static bool test_double_dir_segment(const char *seg, size_t len) { + static const char *const dirs[] = {"fake", "mock", "mocks", "testing", "testutil"}; + for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { + if (strlen(dirs[i]) == len && memcmp(seg, dirs[i], len) == 0) { + return true; + } + } + return false; +} + +/* `base` is a Go file named `prefix`*.go. */ +static bool go_file_with_prefix(const char *base, const char *prefix) { + static const char go_ext[] = ".go"; + size_t blen = strlen(base); + size_t plen = strlen(prefix); + size_t elen = sizeof(go_ext) - SKIP_ONE; + return blen >= plen + elen && strncmp(base, prefix, plen) == 0 && + strcmp(base + blen - elen, go_ext) == 0; +} + +bool cbm_registry_test_double_path(const char *rel_path) { + if (!rel_path) { + return false; + } + const char *seg = rel_path; + for (const char *slash = strchr(seg, '/'); slash; slash = strchr(seg, '/')) { + if (test_double_dir_segment(seg, (size_t)(slash - seg))) { + return true; + } + seg = slash + SKIP_ONE; + } + /* `seg` is the file name now. */ + return go_file_with_prefix(seg, "mock_") || go_file_with_prefix(seg, "fake_"); +} + /* The stored test verdicts. index_under_name drops an entry it cannot record a * verdict for, so this covers every entry of a non-empty bucket. */ static const uint8_t *qn_test_flags(const qn_array_t *arr) { From 7e9186cf8ac9befda5378698364fba5ab7a326e2 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 11 Nov 2026 20:51:42 +0100 Subject: [PATCH 4/5] fix(incremental): carry the test verdict on closure-repair proxies The incremental registry seed reads each stored node's test verdict back with cbm_node_is_test, as the full build takes CBMDefinition.is_test. Main's closure-repair route seeds that registry from cbm_delta_preseed proxies, which load only the properties a resolver reads, and is_test was not among them: every proxy scored as production, so a re-resolved caller could bind a test helper the full build ranks last. On gardener a comment-only edit of three files made the incremental index differ from a fresh full one by two ranking edges (a WRITES into a _test.go function, a USAGE into pkg/utils/test); with this change the only differences left are the 13 similarity / OVERRIDE / IMPLEMENTS edges main already shows on that edit. The preseed now selects whether a row's properties carry "is_test":true and adds just that member to the proxy's properties. Proxies are never written back. If adding it cannot be allocated, the preseed fails like an id mismatch does, rather than seed a proxy that would rank as production. Co-authored-by: Chad <4307099+BobbieBarker@users.noreply.github.com> Signed-off-by: Martin Vogel --- src/pipeline/pipeline_delta.c | 48 ++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 63c5ef1d19..22573f8c8d 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -623,6 +623,28 @@ static char *delta_generated_props(const char *props, const char *generated) { return out; } +/* `props` (a JSON object) with the extractor's test verdict added as + * `"is_test":true` (CBM_MEM_CLASS_OTHER; the caller frees), NULL on allocation + * failure. The incremental registry seed reads the verdict back + * (cbm_node_is_test) so a re-resolved caller ranks a test definition below + * real code exactly as the full build does; an absent key reads as false. */ +static char *delta_test_props(const char *props) { + static const char member[] = "\"is_test\":true"; + size_t pl = props ? strlen(props) : 0; + bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}'; + size_t cap = pl + sizeof(member) + CBM_SZ_4; + char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap); + if (!out) { + return NULL; + } + if (has_members) { + snprintf(out, cap, "%.*s,%s}", (int)(pl - SKIP_ONE), props, member); + } else { + snprintf(out, cap, "{%s}", member); + } + return out; +} + int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) { sqlite3 *db = cbm_store_get_db(store); if (!db) { @@ -673,7 +695,13 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g * And a generated member's mark (a Java record's implicit accessor, a * Method node): the registry seed reads it to register the member by * qualified name only, as the full build does (cbm_node_props_generated); - * without it the proxy came back into the short-name index. */ + * without it the proxy came back into the short-name index. + * + * And the extractor's test verdict: the registry seed reads it back + * (cbm_node_is_test) to rank a test definition below real code, as the + * full build does with CBMDefinition.is_test; without it every proxy + * scored as production and a re-resolved caller could bind a test helper + * the full build ranks last. */ sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2( db, @@ -702,7 +730,8 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g " THEN json_extract(properties, '$.parent_class') END," " CASE WHEN label = 'Method'" " AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0" - " THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END" + " THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END," + " instr(properties, '\"is_test\":true') > 0" " FROM nodes" " WHERE project = ?1 AND label NOT IN" " ('Macro','Comment','Section','Branch','Commit','Tag')" @@ -727,14 +756,27 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g int end_line = sqlite3_column_int(stmt, 7); const char *parent = (const char *)sqlite3_column_text(stmt, 8); const char *generated = (const char *)sqlite3_column_text(stmt, 9); + bool is_test = sqlite3_column_int(stmt, 10) != 0; char *owner_props = parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL; const char *props = owner_props ? owner_props : (client_props ? client_props : "{}"); char *generated_props = delta_generated_props(props, generated); + const char *seed_props = generated_props ? generated_props : props; + char *test_props = is_test ? delta_test_props(seed_props) : NULL; + if (is_test && !test_props) { + /* A proxy without its verdict would rank as production code; + * the run cannot be trusted, as with an id mismatch below. */ + cbm_free(CBM_MEM_CLASS_OTHER, generated_props); + cbm_free(CBM_MEM_CLASS_OTHER, owner_props); + sqlite3_finalize(stmt); + cbm_log_error("delta.preseed_verdict_alloc_failed", "qn", qn ? qn : ""); + return -1; + } /* Pin the gbuf id to the database id: proxies ARE their rows. */ cbm_gbuf_set_next_id(gbuf, id); int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line, - end_line, generated_props ? generated_props : props); + end_line, test_props ? test_props : seed_props); + cbm_free(CBM_MEM_CLASS_OTHER, test_props); cbm_free(CBM_MEM_CLASS_OTHER, generated_props); cbm_free(CBM_MEM_CLASS_OTHER, owner_props); if (got != id) { From cf4f117db469e9d3d456631a927d416a2fedd04d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 11 Nov 2026 20:51:42 +0100 Subject: [PATCH 5/5] test(registry): pin the test-double rule and the verdict through full and incremental - registry_test_double_path_table: the anchored predicate's positives (fake, mock, mocks, testing, testutil directory segments; mock_*.go, fake_*.go) and negatives (inspect, latest, contest, fakeroot, mockery, testingutil, mocking.go, mock_store.py, a file named fake, a different case, NULL). - registry_swift_overload_choice_uses_stored_verdicts: the Swift overload choosers score with the verdicts stored at registration; without them the smaller QN, a test double, wins the tie. - pipeline_registry_ranks_by_extractor_verdict_full_and_incremental: the `latest` vs `_test.go` misranking the QN scan made, through a full index, a closure-repair incremental run and a forced legacy-partial run. - pipeline_registry_ranks_test_doubles_last_full_and_incremental: a generated fake that out-ranks the real package on proximity, through the same three runs. Each fails with the piece it pins reverted (predicate, seeder verdict, incremental seeder rule, closure-repair verdict carry, Swift verdict carry). Co-authored-by: Chad <4307099+BobbieBarker@users.noreply.github.com> Signed-off-by: Martin Vogel --- tests/test_pipeline.c | 128 ++++++++++++++++++++++++++++++++++++++++++ tests/test_registry.c | 64 +++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 55a4ac6176..baf2e34b7d 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6619,6 +6619,132 @@ TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full) { PASS(); } +/* #2427 rank probes: one bare call, two same-named candidates, and the + * registry's test ranking alone decides which one the call binds. Three runs + * on one database: a full index, then a comment-only edit of the caller on + * the closure-repair route, then another on the forced legacy-partial route. + * Both incremental routes re-resolve the caller against a registry seeded from + * stored nodes rather than the extractor's definitions, which is where a + * seeder that loses the verdict shows. */ +typedef struct { + int want[3]; + int other[3]; + cbm_incremental_route_t route[3]; +} rank_probe_t; + +static int rank_probe_count(const char *db, const char *project, const char *source, + const char *target, const char *file) { + cbm_store_t *s = cbm_store_open_path(db); + if (!s) { + return -1; + } + int n = named_edge_to_file_count(s, project, "CALLS", source, target, file); + cbm_store_close(s); + return n; +} + +static void rank_probe(const char *tmp, const char *caller_path, const char *caller_src, + const char *source, const char *target, const char *want_file, + const char *other_file, rank_probe_t *out) { + char db[512]; + snprintf(db, sizeof(db), "%s/rank.db", tmp); + for (int run = 0; run < 3; run++) { + out->want[run] = -1; + out->other[run] = -1; + out->route[run] = CBM_INCREMENTAL_ROUTE_NONE; + if (run > 0) { + char edited[512]; + snprintf(edited, sizeof(edited), "%s// edit %d\n", caller_src, run); + write_temp_file(tmp, caller_path, edited); + } + cbm_pipeline_incremental_test_reset_faults(); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + if (run == 2) { + cbm_pipeline_incremental_test_force_legacy_partial_once(); + } +#endif + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + if (!p) { + return; + } + int rc = cbm_pipeline_run(p); + out->route[run] = cbm_pipeline_incremental_test_last_route(); + char project[256]; + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_pipeline_free(p); + if (rc != 0) { + return; + } + out->want[run] = rank_probe_count(db, project, source, target, want_file); + out->other[run] = rank_probe_count(db, project, source, target, other_file); + } +} + +/* "latest" spells "test", so the QN scan the extractor's verdict replaced + * ranked the production Parse below a helper in a _test.go file, whose Go QN + * names only its package and so spells nothing. The verdict ranks them the + * right way round, and both incremental routes must read the same verdict + * back from the stored nodes: the closure-repair proxies once carried no + * is_test, every candidate scored as production, and the smaller QN -- the + * test helper -- won. */ +TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_verdict_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package app\n\nfunc Run() int { return Parse() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "latest/parse.go", "package latest\n\nfunc Parse() int { return 1 }\n"); + write_temp_file(tmp, "harness/parse_test.go", + "package harness\n\nfunc Parse() int { return 2 }\n"); + write_temp_file(tmp, "app/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "app/run.go", caller, "Run", "Parse", "latest/parse.go", + "harness/parse_test.go", &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + +/* A generated fake beside its callers: client/fake shares more of the caller's + * package path than the real store does, so on the extractor's verdict alone + * (a fake is not a test file) the fake wins on proximity. The test-double path + * rule ranks it last in the full index and, through the incremental seed's own + * use of the rule, on both incremental routes. */ +TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_double_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package client\n\nfunc Run() int { return Load() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "store/load.go", "package store\n\nfunc Load() int { return 1 }\n"); + write_temp_file(tmp, "client/fake/load.go", "package fake\n\nfunc Load() int { return 2 }\n"); + write_temp_file(tmp, "client/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "client/run.go", caller, "Run", "Load", "store/load.go", "client/fake/load.go", + &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + /* A semantic control file can change an unchanged source file's exact target. * Both target modules deliberately export the same short name: without the * tsconfig path mapping, project-wide unique-name fallback is ambiguous and @@ -34486,6 +34612,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity); RUN_TEST(pipeline_complexity_props_independent_of_worker_order); RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full); + RUN_TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental); + RUN_TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental); RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference); RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API diff --git a/tests/test_registry.c b/tests/test_registry.c index 99bb0b5235..10c9ef8e59 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -243,6 +243,68 @@ TEST(registry_sole_test_candidate_still_resolves) { PASS(); } +/* The test-double path rule: whole directory segments named fake, mock, mocks, + * testing or testutil, and Go files named mock_*.go or fake_*.go. Anchored, so + * the substrings the old QN scan tripped on (inspect, latest, contest) and + * near-miss names (fakeroot, mockery, testingutil) stay ordinary code. */ +TEST(registry_test_double_path_table) { + static const char *const doubles[] = { + "pkg/client/fake/clientset.go", + "fake/x.go", + "internal/mocks/store.go", + "pkg/mock/store.go", + "pkg/testing/fixtures.go", + "internal/testutil/helpers.go", + "pkg/mock_store.go", + "pkg/fake_clock.go", + "mock_store.go", + "src/fake/deep/nested/util.py", + }; + static const char *const ordinary[] = { + "pkg/inspect/x.go", "pkg/latest/x.go", "pkg/contest/x.go", "pkg/fakeroot/x.go", + "pkg/mockery/x.go", "pkg/testingutil/x.go", "pkg/mocking.go", "pkg/mock_store.py", + "pkg/fake_clock.golang", "pkg/fake.go", "pkg/x/fake", "pkg/Fake/x.go", + "pkg/myfake/x.go", "pkg/fake-client/x.go", "testing.go", "", + }; + char why[256]; + for (size_t i = 0; i < sizeof(doubles) / sizeof(doubles[0]); i++) { + if (!cbm_registry_test_double_path(doubles[i])) { + snprintf(why, sizeof(why), "expected a test-double path: %s", doubles[i]); + FAIL(why); + } + } + for (size_t i = 0; i < sizeof(ordinary) / sizeof(ordinary[0]); i++) { + if (cbm_registry_test_double_path(ordinary[i])) { + snprintf(why, sizeof(why), "expected an ordinary path: %s", ordinary[i]); + FAIL(why); + } + } + ASSERT_FALSE(cbm_registry_test_double_path(NULL)); + PASS(); +} + +/* The Swift overload choosers score with the verdicts stored at registration, + * carried through their own filtered lists. Both overloads fit the call and tie + * on proximity; without the verdicts the smaller QN (the test double) would win + * the tie. */ +TEST(registry_swift_overload_choice_uses_stored_verdicts) { + cbm_registry_t *r = cbm_registry_new(); + const char *double_qn = "proj.Fixtures.Store.load(path:String)"; + const char *real_qn = "proj.Zeta.Store.load(path:String)"; + cbm_registry_add(r, "load", double_qn, "Method", true); + cbm_registry_add(r, "load", real_qn, "Method", false); + cbm_registry_set_swift_signature(r, double_qn, 0, 1, NULL); + cbm_registry_set_swift_signature(r, real_qn, 0, 1, NULL); + + CBMCallArg arg = {.keyword = "path"}; + CBMCall call = {.callee_name = "load", .args = &arg, .arg_count = 1}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], real_qn); + cbm_registry_free(r); + PASS(); +} + TEST(registry_label_of) { cbm_registry_t *r = cbm_registry_new(); cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class", false); @@ -3382,6 +3444,8 @@ SUITE(registry) { RUN_TEST(registry_prefers_non_test_over_test_candidate); RUN_TEST(registry_test_verdict_comes_from_the_caller_not_the_qn); RUN_TEST(registry_sole_test_candidate_still_resolves); + RUN_TEST(registry_test_double_path_table); + RUN_TEST(registry_swift_overload_choice_uses_stored_verdicts); RUN_TEST(registry_label_of); RUN_TEST(registry_value_target_field_twin); RUN_TEST(registry_find_by_name);