Merge pull request 'fix(halacha): split authority (derived) from rule_role — stop source-conflation (INV-DM7)' (#112) from worktree-halacha-authority-split into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m32s

This commit was merged in pull request #112.
This commit is contained in:
2026-06-07 18:19:43 +00:00
16 changed files with 407 additions and 92 deletions

View File

@@ -0,0 +1,46 @@
"""rule_type coercion after the authority/role split (INV-DM7).
The extractor's rule_type holds the rule ROLE only — it is never defaulted from
the source's bindingness. Legacy authority values fold to the nearest role.
"""
from legal_mcp.services.halacha_extractor import (
_LEGACY_RULE_TYPE_FOLD,
_VALID_RULE_TYPES,
_coerce_halacha,
)
_BASE = {"rule_statement": "כלל כלשהו", "supporting_quote": "ציטוט תומך כלשהו"}
def _rt(rule_type):
return _coerce_halacha({**_BASE, "rule_type": rule_type})["rule_type"]
def test_valid_roles_are_the_five_roles_only():
assert _VALID_RULE_TYPES == {
"holding", "interpretive", "procedural", "application", "obiter",
}
assert "binding" not in _VALID_RULE_TYPES
assert "persuasive" not in _VALID_RULE_TYPES
def test_legacy_authority_values_fold_to_a_role():
assert _rt("binding") == "holding"
assert _rt("persuasive") == "interpretive"
assert _LEGACY_RULE_TYPE_FOLD == {"binding": "holding", "persuasive": "interpretive"}
def test_genuine_roles_pass_through():
for role in ("holding", "interpretive", "procedural", "application", "obiter"):
assert _rt(role) == role
def test_unknown_or_missing_defaults_to_interpretive():
assert _rt("nonsense") == "interpretive"
assert _coerce_halacha(_BASE)["rule_type"] == "interpretive"
def test_coerce_rejects_rows_missing_required_fields():
assert _coerce_halacha({"rule_statement": "x"}) is None
assert _coerce_halacha({"supporting_quote": "y"}) is None
assert _coerce_halacha("not a dict") is None

View File

@@ -211,23 +211,40 @@ def test_application_flag_from_rule_type():
assert hq.FLAG_APPLICATION in flags
def test_application_flag_from_deixis_even_if_binding():
def test_application_flag_from_deixis_even_if_holding():
flags = hq.compute_quality_flags(
"במקרה דנן נדחה הערר", "כפי שקבענו במקרה דנן נדחה הערר",
rule_type="binding",
rule_type="holding",
)
assert hq.FLAG_APPLICATION in flags
def test_clean_binding_rule_has_no_flags():
def test_clean_holding_rule_has_no_flags():
flags = hq.compute_quality_flags(
"ועדת הערר מוסמכת לדון בטענות חוקתיות הנוגעות לתכנית",
"הוועדה מוסמכת לדון אף בטענות מסוג זה, ככל שהן נוגעות לתכנית שבנדון.",
rule_type="binding",
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():