"""`score` and `relevance` are different things — thresholds must use `relevance`. Retrieval returns cosine similarities (~0.4-0.75) until the lexical leg returns rows; then `_merge_sem_lex` replaces `score` with an RRF value (~0.008-0.02). Both are legitimate *ranking* signals, but they are not on the same scale, so a caller comparing `score >= 0.45` rejected every hit the moment BM25 matched anything. That is how the citation-verification tab came to show no supporting precedent for a single argument, on every case. `relevance` is the fix: always a cosine, or None when the row came from the lexical leg alone and no cosine was ever computed. """ import pytest from legal_mcp.services.case_citation_verification import _SUGGEST_FLOOR, _passes_floor from legal_mcp.services.hybrid_search import _merge_sem_lex def _sem(key: str, score: float) -> dict: return {"chunk_id": key, "case_law_id": "c1", "score": score, "relevance": score} def _lex(key: str, score: float) -> dict: # The lexical leg emits ts_rank_cd, never a cosine — so no `relevance`. return {"chunk_id": key, "case_law_id": "c1", "score": score} def test_fusion_replaces_score_but_preserves_the_cosine(): """The regression in one assertion.""" merged = _merge_sem_lex([_sem("a", 0.73)], [_lex("a", 0.31)], limit=10) row = merged[0] assert row["score"] < 0.1, "fused score is an RRF value, not a cosine" assert row["relevance"] == pytest.approx(0.73), "the cosine must survive fusion" def test_lexical_only_row_has_no_fabricated_cosine(): """None means 'not measured' — not 'measured as irrelevant'.""" merged = _merge_sem_lex([], [_lex("b", 0.31)], limit=10) assert merged[0]["relevance"] is None def test_semantic_only_row_keeps_its_cosine(): merged = _merge_sem_lex([_sem("c", 0.62)], [], limit=10) assert merged[0]["relevance"] == pytest.approx(0.62) def test_floor_would_have_rejected_everything_on_the_fused_score(): """Guards the exact production symptom: fused scores are ~0.008, the floor is 0.45, so score-based filtering wipes the result set.""" merged = _merge_sem_lex( [_sem(k, 0.70) for k in "abcd"], [_lex(k, 0.30) for k in "abcd"], limit=10) assert all(r["score"] < _SUGGEST_FLOOR for r in merged) # the bug assert all(_passes_floor(r) for r in merged) # the fix def test_floor_still_rejects_genuinely_weak_hits(): """The fix must not become 'accept everything'.""" assert not _passes_floor({"relevance": 0.10}) assert not _passes_floor({"relevance": _SUGGEST_FLOOR - 0.01}) assert _passes_floor({"relevance": _SUGGEST_FLOOR}) def test_lexical_only_hits_are_kept_deliberately(): """An exact docket/phrase match reaches the top by BM25 rank with no cosine. Dropping it would hide precisely what a chair searches for by name.""" assert _passes_floor({"relevance": None}) assert _passes_floor({}) # missing key behaves the same as None