`search_precedent_library` — הכלי שסוכני הכתיבה קוראים לו בפועל — לא חשף `source_kind` כלל, וכל שכבה מתחתיו נפלה לברירת-מחדל `external_upload`. התוצאה: `WHERE cl.source_kind = 'external_upload'` חתך 104 החלטות ועדות-ערר (29% מהקורפוס) עוד לפני הדירוג. ערר (חיפה) 83/16 — האסמכתה הישירה ביותר בקורפוס לטענת-סף ס-2 ב-1069-04-26 — לא הוחזרה גם בשאילתה כמעט-מילולית. ההיפותזה שנרשמה ב-#232 (הטיית verified/cite_count מדירה החלטות חדשות) **נבדקה ונפסלה**: כיסוי ה-verified כמעט זהה בשני הקורפוסים (55% מול 49%), ותקרת-ההטיה 0.22 לא הסבירה פער מול התאמה כמעט-מילולית. הסיבה הייתה סינום קשיח, לא דירוג. מה שונה - `_source_kind_clause()` — הגדרה אחת לאוצר-המילים של הסלקטור (G2), במקום שכפולו בכל אתר חיפוש/רשימה. `""`/`"all"` = כל הקורפוס; סלקטור לא-מוכר מרים ValueError במקום להגיע ל-SQL (הערך מוזרק ב-f-string, אז ה-whitelist הוא גם מה ששומר על זה בטוח). - ברירת-המחדל בכל שרשרת-החיפוש והרשימה: `""` = הקורפוס כולו. - `search_precedent_library` + `precedent_library_list` חושפים `source_kind` לסינון מפורש; `/api/precedent-library/search` מקבל אותו גם הוא כדי ש-UI ו-MCP לא יתפצלו. - נרמול מפריד במספר-תיק: `83/16` ו-`83-16` מחזירים את אותה שורה. אימות מול ה-DB החי — 4/4 החלטות ועדה חוזרות במקום **1** בשאילתה בלשון הלכה מאושרת (83-16, 1029-18, 1085-23, 1094-09-19 פדילה). 83/16 עלתה ל-0.711 מול 0.663 של 3213/97 שחסמה אותה קודם. רשימת-הקורפוס: 259 → 386. invariants: G1 (נרמול במקור, לא תיקון-תסמין בקריאה) · G2 (הגדרה אחת לסלקטור; UI ו-MCP על אותו מסלול) · §6 (סלקטור שגוי מתפוצץ, לא נבלע) טסטים: 5 חדשים (tests/test_source_kind_selector.py), אחד מהם נועל את ברירות-המחדל של 8 נקודות-הכניסה — זה בדיוק הבאג. 514 עוברים.
68 lines
2.6 KiB
Python
68 lines
2.6 KiB
Python
"""#232 — the source_kind selector must not silently hide a corpus.
|
|
|
|
`search_precedent_library` defaulted to source_kind='external_upload' all the
|
|
way down the stack, so 104 appeals-committee decisions (29% of the corpus)
|
|
were unreachable through the entry point the writing agents actually call.
|
|
These tests pin the selector semantics; the end-to-end retrieval check lives
|
|
in scripts/test_retrieval_by_name.py (needs a live DB).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from legal_mcp.services.db import _source_kind_clause
|
|
|
|
|
|
def test_empty_selector_means_no_filter():
|
|
"""'' and 'all' must produce no predicate — the whole corpus."""
|
|
assert _source_kind_clause("") == ""
|
|
assert _source_kind_clause("all") == ""
|
|
assert _source_kind_clause(" ") == ""
|
|
|
|
|
|
def test_named_kinds_produce_equality_predicate():
|
|
assert _source_kind_clause("internal_committee", "cl.") == (
|
|
"cl.source_kind = 'internal_committee'"
|
|
)
|
|
assert _source_kind_clause("external_upload") == "source_kind = 'external_upload'"
|
|
|
|
|
|
def test_all_committees_expands_to_both_shapes():
|
|
"""Committee decisions live under two historical shapes — cover both."""
|
|
clause = _source_kind_clause("all_committees", "cl.")
|
|
assert "cl.source_kind = 'internal_committee'" in clause
|
|
assert "cl.source_type = 'appeals_committee'" in clause
|
|
assert clause.startswith("(") and clause.endswith(")")
|
|
|
|
|
|
def test_unknown_selector_raises_rather_than_reaching_sql():
|
|
"""The value is f-string-interpolated, so the whitelist is the guard."""
|
|
with pytest.raises(ValueError, match="source_kind"):
|
|
_source_kind_clause("'; DROP TABLE case_law; --")
|
|
with pytest.raises(ValueError):
|
|
_source_kind_clause("internal")
|
|
|
|
|
|
def test_default_of_the_search_entry_points_is_whole_corpus():
|
|
"""A regression guard on the defaults themselves — this is the bug."""
|
|
import inspect
|
|
|
|
from legal_mcp.services import hybrid_search, precedent_library
|
|
from legal_mcp.services import db as db_mod
|
|
from legal_mcp.tools import precedent_library as plib_tool
|
|
|
|
for fn in (
|
|
db_mod.search_precedent_library_semantic,
|
|
db_mod.search_precedent_library_lexical,
|
|
db_mod.list_external_case_law,
|
|
hybrid_search.search_precedent_library_hybrid,
|
|
precedent_library.search_library,
|
|
precedent_library.list_precedents,
|
|
plib_tool.search_precedent_library,
|
|
plib_tool.precedent_library_list,
|
|
):
|
|
default = inspect.signature(fn).parameters["source_kind"].default
|
|
assert default == "", (
|
|
f"{fn.__module__}.{fn.__qualname__} defaults source_kind to "
|
|
f"{default!r} — that hides a corpus from every caller (#232)"
|
|
)
|