Bake the 2026-06-03 strict-cleanup rubric into the extraction pipeline so the corpus stays clean at the source instead of accumulating duplicates, obiter dicta, truncated quotes and thin restatements that clog the review queue. #81 — quality gate: - New pure module halacha_quality.py with unit-tested validators: non-decision/obiter (Wambaugh markers), truncated-quote (mid-word cut), thin-restatement (rule≈quote), quote-unverified. - Validators run in halacha_extractor._process; a non-decision is re-typed obiter; flags persist in new halachot.quality_flags column. - Auto-approve now requires confidence>=threshold AND no quality flags; flagged items route to pending_review regardless of confidence. - Both extraction prompts hardened: reject undecided dicta, exclude case-specific applications, require abstraction, forbid over-splitting. #82 — dedup-on-insert (store_halachot_for_chunk): - Within the same precedent, skip a halacha whose normalized supporting_quote already exists, or whose rule-embedding has cosine>=HALACHA_DEDUP_COSINE (0.93) against an already-stored one. Makes re-runs idempotent. Migration: halachot.quality_flags TEXT[] (additive, idempotent ALTER). Tests: 19 new unit tests; full suite 156 passed. Validated end-to-end against dev DB (dedup skips dups, flag blocks auto-approve, re-run inserts 0). Calibration: flags fire on only ~10% of current survivors (low false-positive). Spec: docs/halacha-strict-rubric.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
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")
|