Repository navigation
fix(registry): score a candidate on the extractor's test verdict, not its name - #2427
BobbieBarker wants to merge 2 commits into
Conversation
… 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>
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Thank you for quantifying the retargeted and lost edges instead of presenting only a green suite. I checked is_test_qn and candidate scoring on current main; the name-based classification is indeed part of candidate selection. Because this changes winners across languages and indexing modes, we need more time to review the recorded edge changes and incremental behavior before deciding. It is tagged as a parsing-quality bug for deeper review. Thank you for your patience with the full queue. |
No problem, I think you're doing a great job considering the popularity of the repo and how hard OS is to do right. 👍 |
|
Thanks for the kind follow-up and for quantifying the candidate retargets. The cross-language scoring and incremental behavior still need maintainer review. No further work is requested from you in this sweep. |
|
Thank you for the test-verdict scoring change and the measured retargets. The cross-language and incremental review remains on us, with no additional request for you. Sorry for the delay—I’ll follow up soon. |
DeusData
left a comment
There was a problem hiding this comment.
Thank you, Chad, for this careful change, and for measuring it instead of trusting a green suite. You are right about the root problem: an unanchored substring scan over the qualified name is wrong in both directions, and inspect, latest and contest are exactly the false positives that make it unreliable. Taking the extractor's verdict instead of re-deriving it is the right foundation, and carrying the verdicts through the filtered resolve paths fixes a real silent fallback. Sorry it took us this long to come back with a decision; this is the follow-up we promised.
The decision: we want this approach. CBMDefinition.is_test becomes the verdict and is_test_qn goes away. We also measured your branch against its base on a large Go project (gardener, about 2,400 non-test Go files) and found one effect your four corpora could not show. Go projects keep many test doubles in ordinary, non-test files: generated fake clientsets, mock packages and mock_*.go files. The old name scan happened to rank those below real code; the extractor's verdict does not mark them as test code. With the change, about 570 call edges newly resolve into fake or mock files (73 of them from production code, for example an init() whose AddToScheme call now lands in the generated fake clientset), and none move out of them. About 3% of all CALLS edges move on that project. So we would like one anchored rule on top of your verdict, and then a port onto current main:
-
Keep
CBMDefinition.is_testas the primary verdict, exactly as you have it, and keep the deletion ofis_test_qn. -
Add a test-double path rule, anchored to whole path segments and file-name prefixes rather than substrings, so it can never match
inspect,latestorfakeroot. A candidate counts as test code for this penalty when the repo-relative path of its defining file has a directory segment named exactlyfake,mock,mocks,testingortestutil, or when the file name matchesmock_*.goorfake_*.go. That is the complete list. -
Keep the rule in one predicate that takes the defining file's path, next to the scoring in
src/pipeline/registry.cand declared inpipeline.h, and have all three registry seeders pass the extractor verdict OR that predicate:pass_definitions.candpass_parallel.cwith the definition's file path, and the incremental seeder inpipeline_incremental.cwithcbm_node_is_test(node)andnode->file_path. One predicate keeps the full and incremental registries answering the same way. Please keep it out ofcbm_is_test_pathandcbm_is_test_file: those decide the storedis_testproperty, TESTS edges and importance, and a generated fake clientset is not a test. This rule only changes how a candidate is ranked when a call is resolved, never what a node is. -
Port the branch onto current main (rebase or merge, whichever you prefer, with sign-offs as before). The registry has changed a lot since your base and five files conflict today, mostly
registry.c. The main differences:- registration now goes through
cbm_registry_add_lang_in_module(language plus module prefix), withcbm_registry_addandcbm_registry_add_langas thin wrappers, sois_testneeds to be threaded through that function; - each name bucket now carries a
langbyte array besideis_test, grown in lockstep and sharingis_test_cap, so your drop-on-failed-growth rule inindex_under_nameshould cover both arrays; best_by_import_distancenow takes the registry and the language array, and more resolve paths call it. The fuzzy path on main still passes NULL verdicts whenever it has filtered by imports, so yourfilter_import_reachablefix is still needed and should carry the languages along with the verdicts.
- registration now goes through
-
Tests that pin the rule, next to your three registry tests:
- a table test of the predicate: positives such as
pkg/client/fake/clientset.go,fake/x.go,internal/mocks/store.go,pkg/testing/fixtures.go,internal/testutil/helpers.go,pkg/mock_store.goandpkg/fake_clock.go; negatives such aspkg/inspect/x.go,pkg/latest/x.go,pkg/fakeroot/x.go,pkg/mockery/x.go,pkg/testingutil/x.go,pkg/mocking.goandpkg/mock_store.py; - a resolution test in which a production caller has two same-named candidates, a real one and one defined under
fake/whose extractor verdict is false, and the real one wins, through both the full and the incremental registry (the incremental one is where a missed seeder would show); - please check that each new test fails with the rule removed, as you did for
registry_test_verdict_comes_from_the_caller_not_the_qn, and that itsRUN_TESTline is in the suite.
- a table test of the predicate: positives such as
What we will do on our side:
- Once the ported head is up, we will re-run the same gardener comparison on it and post the numbers here, so you do not need a large Go corpus yourself.
- If the port turns out to be painful, just say so. Maintainer edits are enabled on your branch, so we can take the main merge, or the whole port, on top of your commits with your authorship kept.
Thank you again for the thorough work, and for your patience with the queue.
This PR was written by an AI agent working on my behalf. I certify the DCO sign-off on the commit and answer for the change.
Follow-up to #2370. The registry scores candidates on
CBMDefinition.is_testinstead of scanning the qualified name, andis_test_qnis deleted.is_test_qnmatchedtest,Test,mock,stub,spec,fakeorFixtureanywhere in the QN, andREG_TEST_PENALTYuses that to pick which definition an unresolved call resolves to. On one 970-file Elixir tree it calls 794 of 20,882 Function and Method nodes test code. 516 match onspec, 268 of those only becauseinspectcontains it:inspect_optional,inspect_record,inspection_actions.latestandcontestmatchtestthe same way, and a test helper under a directory spelling none of those words is scored as production.Cost
Four corpora, a binary from this commit's parent and one from this branch,
edgesdiffed on (source QN, target QN, type):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 and which moves on its own.acceptance_spec_runneris a typical retarget:resultresolved tocritique_report.resultand now resolves tooperator_inspect.reader.result, the function it calls.This is a behaviour change in every indexed project, not a signature change.
Ripples
candidate_scoreloses its-1: asktri-state andqn_test_flagsstops returning NULL, since there is nothing left to recompute from.Both filtered resolve paths now carry verdicts with their candidates.
resolve_multi_with_importscopied a parallel array but guarded it onflags ?;filter_import_reachablecarried none, so the fuzzy path passed NULL whenever it had filtered and fell back to the scan silently.index_under_namedrops an entry whose verdict it cannot store rather than leaving the array short, because a missing verdict scores as non-test and would outrank real code.pipeline_incremental.cseeds from persisted nodes with noCBMDefinition, so it reads the flag out ofproperties_jsonviacbm_node_is_test, exported frompass_tests.c.Tests
REG_TEST_PENALTYhad no direct coverage: of the 95cbm_registry_addcall sites in the suite, none registered a symbol the scan would flag, so the suite passes with the scan and without it.registry_test_verdict_comes_from_the_caller_not_the_qndiscriminates:proj.latest.parseagainstproj.harness.parse. With the scan restored and the signature kept it fails on"proj.harness.parse" != "proj.latest.parse". The other two pin that the penalty exists and that it orders candidates without vetoing them.144 of 144 suites, 8,254 passed, 0 failed, 10 skipped.