Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/server/src/repowise/server/mcp_server/tool_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,23 @@ async def _structured_search(
files: list[dict] = []
concepts: list[dict] = []

# A hybrid query is prose wrapped around an identifier ("where is X
# defined"). The symbol scorer ranks on token overlap, so handing it the
# raw prose lets stopword-ish tokens ("is" -> is_ci, "filter" ->
# FilterRegistry) outrank the identifier the question is actually about,
# which then never reaches _has_exact_symbol and the response claims the
# symbol is unindexed. Score symbols on the extracted identifiers instead.
symbol_query = query
if mode == "hybrid":
_idents = _embedded_identifiers(query)
if _idents:
symbol_query = " ".join(_idents)

for ctx in contexts:
if mode in ("symbol", "hybrid"):
s = await search_symbols_single(ctx, query, limit, symbol_kind=symbol_kind, kind=kind)
s = await search_symbols_single(
ctx, symbol_query, limit, symbol_kind=symbol_kind, kind=kind
)
_tag_repo(s, ctx, multi)
symbols.extend(s)
if mode == "path":
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/server/mcp/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,38 @@ def test_identifier_candidates_by_mode(self):
assert _identifier_candidates("where is AuthService defined", "hybrid") == ["AuthService"]
assert _identifier_candidates("rate limiting", "concept") == []

@pytest.mark.asyncio
async def test_hybrid_scores_symbols_on_the_identifier_not_the_prose(
self, setup_mcp, monkeypatch
):
# Regression: the symbol scorer ranks on token overlap, so handing it the
# whole question let prose tokens ("is" -> is_ci, "filter" ->
# FilterRegistry) outrank the identifier the question asks after. The
# identifier then never reached _has_exact_symbol and the response
# claimed an indexed symbol was absent.
#
# Asserted on the query the scorer RECEIVES, not on ranking: a fixture
# small enough to unit-test has no decoys to lose to, so an end-to-end
# assertion here passes against the bug.
from repowise.server.mcp_server import search_codebase, tool_search

seen: list[str] = []
real = tool_search.search_symbols_single

async def spy(ctx, query, limit, **kwargs):
seen.append(query)
return await real(ctx, query, limit, **kwargs)

monkeypatch.setattr(tool_search, "search_symbols_single", spy)

await search_codebase("where is AuthService defined")
assert seen == ["AuthService"], f"symbol scorer got prose, not the identifier: {seen}"

# A query carrying no identifier still scores on the raw text.
seen.clear()
await search_codebase("AuthService", mode="symbol")
assert seen == ["AuthService"]

@pytest.mark.asyncio
async def test_exact_hit_sets_true_and_no_note(self, setup_mcp):
from repowise.server.mcp_server import search_codebase
Expand Down
Loading