Skip to content

fix(registry): score a candidate on the extractor's test verdict, not its name - #2427

Open
BobbieBarker wants to merge 1 commit into
DeusData:mainfrom
BobbieBarker:fix/registry-test-bit
Open

BobbieBarker wants to merge 1 commit into
DeusData:mainfrom
BobbieBarker:fix/registry-test-bit

Conversation

@BobbieBarker

@BobbieBarker BobbieBarker commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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_test instead of scanning the qualified name, and is_test_qn is deleted.

is_test_qn matched test, Test, mock, stub, spec, fake or Fixture anywhere in the QN, and REG_TEST_PENALTY uses 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 on spec, 268 of those only because inspect contains it: inspect_optional, inspect_record, inspection_actions. latest and contest match test the 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, edges diffed on (source QN, target QN, type):

corpus moved retargeted lost (resolution) lost (similarity)
Elixir, 970 files 920 919 1 0
ripgrep 14.1.1 353 193 30 130
tokio 1.40.0 1,548 1,426 86 36
rust-analyzer 2024-09-30 5,160 4,817 219 124

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_runner is a typical retarget: result resolved to critique_report.result and now resolves to operator_inspect.reader.result, the function it calls.

This is a behaviour change in every indexed project, not a signature change.

Ripples

candidate_score loses its -1: ask tri-state and qn_test_flags stops returning NULL, since there is nothing left to recompute from.

Both filtered resolve paths now carry verdicts with their candidates. resolve_multi_with_imports copied a parallel array but guarded it on flags ?; filter_import_reachable carried none, so the fuzzy path passed NULL whenever it had filtered and fell back to the scan silently.

index_under_name drops 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.c seeds from persisted nodes with no CBMDefinition, so it reads the flag out of properties_json via cbm_node_is_test, exported from pass_tests.c.

Tests

REG_TEST_PENALTY had no direct coverage: of the 95 cbm_registry_add call 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_qn discriminates: proj.latest.parse against proj.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.

… 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>
@github-actions

Copy link
Copy Markdown

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. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant