Files
legal-ai/mcp-server/tests/test_halacha_quality.py
Chaim 42376db4c5
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s
fix(chunker): תיקון זיהוי כותרות טענות הצדדים ביחיד/נקבה + שלוש שכבות הגנה
**סיבת-שורש:** רג'קס respondent_claims כיסה רק צורת רבים (המשיבים/המשיבין),
ולא יחיד נקבה (המשיבה) ויחיד זכר (המשיב). הכותרת "טענות המשיבה:" בתיק 8181-21
נבלעה לתוך מקטע ruling → חולצה כהלכה שגויה.

**שלוש שכבות הגנה:**
1. chunker.py — הרחבת SECTION_PATTERNS לכסות יחיד/זכר/נקבה + תגובת/תשובת
2. halacha_extractor.py — עיגון חיובי: drop ל-ruling chunks שלפני legal_analysis
3. halacha_quality.py — FLAG_PARTY_CLAIM: זיהוי שפת-טענות-צד בציטוט התומך

**היקף:** 93 תיקים עם 0 chunks של טענות (כנראה בגלל כותרות ביחיד שלא הוכרו);
628 הלכות מאושרות מתיקים אלה — חלקן עשויות להיות תקינות, יש לעשות re-chunk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 15:12:05 +00:00

325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import pytest
from legal_mcp.services import halacha_quality as hq
# ── non-decision / obiter ──
@pytest.mark.parametrize("text", [
"איני רואה לקבוע מסמרות בשאלה זו",
"אין צורך להכריע בטענה זו",
"למעלה מן הצורך נעיר כי",
"הערה זו ניתנת אגב אורחא",
])
def test_detect_non_decision_hits(text):
assert hq.detect_non_decision(text) is not None
@pytest.mark.parametrize("text", [
"בית המשפט קבע כי ההיתר בטל",
"ועדת הערר מוסמכת לדון בטענת סטייה מתכנית",
"",
])
def test_detect_non_decision_misses(text):
assert hq.detect_non_decision(text) is None
def test_non_decision_scans_all_fields():
# marker sits in the quote, not the abstracted rule
assert hq.detect_non_decision("כלל כללי", "", "וכאן אין צורך להכריע") is not None
# ── truncated quote ──
def test_truncated_dangling_letter():
assert hq.is_quote_truncated("ראוי כי תהיה השפעה על ה") is True
def test_truncated_empty():
assert hq.is_quote_truncated(" ") is True
@pytest.mark.parametrize("quote", [
"ועדת הערר היא הגוף המקצועי האמון על בחינת ההיבטים התכנוניים.",
"אין לועדה סמכות לסטות מתקנות התכנון והבניה", # no period, but full word
"ההיתר תואם את התכנית החלה על האיזור",
])
def test_not_truncated_complete_clauses(quote):
assert hq.is_quote_truncated(quote) is False
# ── thin restatement ──
def test_thin_restatement_near_copy():
quote = "ביטול היתר מחייב טעמים כבדי משקל של אינטרס ציבורי"
rule = "ביטול היתר מחייב טעמים כבדי משקל של אינטרס ציבורי"
assert hq.is_thin_restatement(rule, quote) is True
def test_not_thin_when_abstracted():
quote = "אין חולק כי אין לועדה סמכות לסטות מתקנות"
rule = ("ועדה מקומית לתכנון ובניה אינה מוסמכת לסטות מהוראות תקנות התכנון "
"והבניה, ובכלל זה מהוראות התוספת השנייה, ואין בידה ליתן היתר הסוטה מהן.")
assert hq.is_thin_restatement(rule, quote) is False
def test_thin_handles_empty():
assert hq.is_thin_restatement("", "something") is False
# ── aggregate flags + auto-approve gate semantics ──
def test_clean_halacha_no_flags():
rule = ("ועדת הערר מוסמכת לדון בערר על החלטה ליתן היתר בנייה גם כאשר נטען "
"כי ההיתר סוטה מתכנית, בהתאם למגמת תיקון 43 לחוק.")
quote = ("פרשנות מרחיבה המאפשרת הגשת ערר גם במקרה של מתן היתר כאשר נטען כי "
"ההיתר סוטה מתכנית הולמת את מגמת המחוקק בתיקון 43.")
assert hq.compute_quality_flags(rule, quote, "", quote_verified=True) == []
def test_flags_accumulate():
flags = hq.compute_quality_flags(
"כלל אגב אורחא על ה", "כלל אגב אורחא על ה",
quote_verified=False,
)
assert hq.FLAG_NON_DECISION in flags
assert hq.FLAG_TRUNCATED_QUOTE in flags
assert hq.FLAG_QUOTE_UNVERIFIED in flags
def test_normalize_text_quote_variants():
assert hq.normalize_text('עע"מ 317/10') == hq.normalize_text("עע״מ 317/10")
# ── #81.3 NLI entailment — pure prompt + parser ──
def test_build_nli_prompt_contains_pairs():
items = [
{"rule_statement": "כלל אלף", "supporting_quote": "ציטוט אלף"},
{"rule_statement": "כלל בית", "supporting_quote": "ציטוט בית"},
]
p = hq.build_nli_prompt(items)
assert "כלל אלף" in p and "ציטוט בית" in p
assert "זוג 1" in p and "זוג 2" in p
@pytest.mark.parametrize("raw,n,expected", [
(["entailed", "neutral"], 2, ["entailed", "neutral"]),
(["ENTAILED", "Contradiction"], 2, ["entailed", "contradiction"]), # case-insensitive
([{"verdict": "neutral"}, {"verdict": "entailed"}], 2, ["neutral", "entailed"]), # dict shape
(["entailed"], 2, ["entailed", "entailed"]), # length mismatch -> fail-open
(None, 2, ["entailed", "entailed"]), # non-list -> fail-open
(["bananas", "neutral"], 2, ["entailed", "neutral"]), # unknown label -> entailed
])
def test_parse_nli_verdicts(raw, n, expected):
assert hq.parse_nli_verdicts(raw, n) == expected
# ── _nli_check (async, via claude_session) — fail-open + verdict mapping ──
def test_nli_check_fail_open(monkeypatch):
import asyncio
from legal_mcp.services import halacha_extractor as he
async def boom(*a, **k):
raise RuntimeError("no claude CLI here")
monkeypatch.setattr(he.claude_session, "query_json", boom)
items = [{"rule_statement": "a", "supporting_quote": "b"}]
assert asyncio.run(he._nli_check(items)) == ["entailed"] # never blocks
def test_nli_check_maps_verdicts(monkeypatch):
import asyncio
from legal_mcp.services import halacha_extractor as he
async def fake(*a, **k):
return ["entailed", "neutral"]
monkeypatch.setattr(he.claude_session, "query_json", fake)
items = [{"rule_statement": "a", "supporting_quote": "b"},
{"rule_statement": "c", "supporting_quote": "d"}]
assert asyncio.run(he._nli_check(items)) == ["entailed", "neutral"]
def test_nli_check_empty():
import asyncio
from legal_mcp.services import halacha_extractor as he
assert asyncio.run(he._nli_check([])) == []
# ── #81.5 consolidation — pure prompt + fold-group parser ──
def test_build_consolidation_prompt():
items = [
{"halacha_index": 3, "rule_statement": "כלל גימל", "reasoning_summary": "כי"},
{"halacha_index": 7, "rule_statement": "כלל זין", "reasoning_summary": ""},
]
p = hq.build_consolidation_prompt(items)
assert "[3] כלל גימל" in p and "[7] כלל זין" in p and "היגיון: כי" in p
@pytest.mark.parametrize("raw,expected", [
([[2, 5, 9], [14, 18]], [[2, 5, 9], [14, 18]]),
([[2, 5], [7]], [[2, 5]]), # singleton group dropped
([["2", "5"]], [[2, 5]]), # string ints coerced
([[2, 2, 5]], [[2, 5]]), # dedup within group
([], []), # nothing to fold
("garbage", []), # non-list -> safe
(None, []), # None -> safe
([[1, "x"], [3, 4]], [[3, 4]]), # drop group that falls below 2 valid
])
def test_parse_fold_groups(raw, expected):
assert hq.parse_fold_groups(raw) == expected
def test_consolidation_priority_prefers_approved_then_confidence():
from legal_mcp.services import halacha_extractor as he
approved = {"id": "a", "review_status": "approved", "confidence": 0.7,
"quote_verified": True, "rule_statement": "x"}
pending_hi = {"id": "b", "review_status": "pending_review", "confidence": 0.95,
"quote_verified": True, "rule_statement": "x"}
# approved sorts before higher-confidence pending → kept as canonical
assert min([approved, pending_hi], key=he._consolidation_priority)["id"] == "a"
# ── #81.4 fact-dependent / application ──
@pytest.mark.parametrize("rule", [
"במקרה דנן ועדת הערר קבעה כי ההיתר בטל",
"בענייננו אין הצדקה לפיצוי",
"בערר שלפנינו הוכח כי השומה שגויה",
])
def test_is_fact_dependent_hits(rule):
assert hq.is_fact_dependent(rule) is True
@pytest.mark.parametrize("rule", [
"ועדת הערר מוסמכת לדון בהיטל השבחה",
"נטל ההוכחה מוטל על המבקש",
"פגיעה תכנונית מזכה בפיצוי לפי סעיף 197",
])
def test_is_fact_dependent_misses(rule):
assert hq.is_fact_dependent(rule) is False
def test_application_flag_from_rule_type():
flags = hq.compute_quality_flags(
"נטל ההוכחה על המבקש", "נטל ההוכחה על המבקש כאמור",
rule_type="application",
)
assert hq.FLAG_APPLICATION in flags
def test_application_flag_from_deixis_even_if_holding():
flags = hq.compute_quality_flags(
"במקרה דנן נדחה הערר", "כפי שקבענו במקרה דנן נדחה הערר",
rule_type="holding",
)
assert hq.FLAG_APPLICATION in flags
def test_clean_holding_rule_has_no_flags():
flags = hq.compute_quality_flags(
"ועדת הערר מוסמכת לדון בטענות חוקתיות הנוגעות לתכנית",
"הוועדה מוסמכת לדון אף בטענות מסוג זה, ככל שהן נוגעות לתכנית שבנדון.",
rule_type="holding",
)
assert flags == []
# ── INV-DM7: authority is DERIVED from the source, never a rule_type value ──
def test_derive_authority_binding_for_higher_courts():
assert hq.derive_authority("עליון") == "binding"
assert hq.derive_authority("מנהלי") == "binding"
def test_derive_authority_persuasive_for_committee():
assert hq.derive_authority("ועדת_ערר_מחוזית") == "persuasive"
def test_derive_authority_none_for_unknown_or_empty():
assert hq.derive_authority("") is None
assert hq.derive_authority(None) is None
assert hq.derive_authority("משהו אחר") is None
# ── #82.3 lexical near-duplicate signal ──
def test_jaccard_high_for_reworded_same_rule():
a = "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית"
b = "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית בלבד"
assert hq.jaccard_shingles(a, b) >= 0.5
def test_jaccard_low_for_distinct_rules():
a = "ועדת הערר מוסמכת לדון בהיטל השבחה"
b = "המועד להגשת ערר הוא שלושים יום"
assert hq.jaccard_shingles(a, b) < 0.2
def test_normalized_levenshtein_identical_and_disjoint():
assert hq.normalized_levenshtein("אבג", "אבג") == 1.0
assert hq.normalized_levenshtein("", "אבג") == 0.0
def test_lexical_near_duplicate_band():
a = "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית"
b = "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית, כך נפסק"
assert hq.lexical_near_duplicate(a, b) is True
c = "המועד להגשת ערר על שומה הוא שלושים ימים"
assert hq.lexical_near_duplicate(a, c) is False
# ── party-claim language detector (FLAG_PARTY_CLAIM) ────────────────────────
@pytest.mark.parametrize("quote", [
# Named-party attribution — always party-claim language
"לטענת העורר אין בסיס לחיוב בהיטל",
"לטענת המשיבה יש לדחות את הערר",
"לטענת המשיב לא הייתה כל השבחה",
"לטענת המשיבים ההחלטה תקינה",
"טוענת המשיבה כי אין מקום לפיצוי",
])
def test_detect_party_claim_language_hits(quote):
assert hq.detect_party_claim_language(quote) is not None, (
f"Quote '{quote[:40]}' should trigger party_claim detection"
)
@pytest.mark.parametrize("quote", [
# Court's own reasoning — must NOT trigger the flag
"ועדת הערר קובעת כי ההיתר תואם את התכנית",
"מן הטעמים האמורים, הערר מתקבל",
"לאחר בחינת הטענות אנו סבורים כי יש לדחות",
# Broad passive forms courts also use in their reasoning (excluded from markers)
"נטען כי ההיתר ניתן שלא כדין",
"נטען על ידי העורר כי השומה שגויה",
# The 8181-21 bad halacha quote — no party-claim markers
"גם אם ניתן בעבר פטור לעוררת על בסיס פרשנות הוראות החוק שהשתנתה כיום הרי שיש לפעול בהתאם לפרשנות כיום",
])
def test_detect_party_claim_language_misses(quote):
assert hq.detect_party_claim_language(quote) is None, (
f"Quote '{quote[:40]}' must NOT trigger party_claim detection"
)
def test_party_claim_flag_in_compute_quality_flags():
flags = hq.compute_quality_flags(
"ביטול היתר מחייב בסיס חוקי",
"לטענת העורר ההיתר ניתן שלא כדין ויש לבטלו",
quote_verified=True,
)
assert hq.FLAG_PARTY_CLAIM in flags
def test_court_quote_does_not_trigger_party_claim_flag():
flags = hq.compute_quality_flags(
"ביטול היתר מחייב בסיס חוקי מוצק",
"ועדת הערר קובעת כי ביטול היתר מחייב בסיס חוקי מוצק בדמות פגיעה ממשית באינטרס ציבורי",
quote_verified=True,
rule_type="holding",
)
assert hq.FLAG_PARTY_CLAIM not in flags