From 09eec6a906efe3bfc50b327903525067ee3f311e Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 31 May 2026 18:54:50 +0000 Subject: [PATCH] feat(corroboration): treatment classifier + polarity (INV-COR2, X11) Co-Authored-By: Claude Sonnet 4.6 --- .../src/legal_mcp/services/corroboration.py | 52 +++++++++++++++++++ mcp-server/tests/test_corroboration.py | 17 ++++++ 2 files changed, 69 insertions(+) create mode 100644 mcp-server/src/legal_mcp/services/corroboration.py create mode 100644 mcp-server/tests/test_corroboration.py diff --git a/mcp-server/src/legal_mcp/services/corroboration.py b/mcp-server/src/legal_mcp/services/corroboration.py new file mode 100644 index 0000000..78ad88a --- /dev/null +++ b/mcp-server/src/legal_mcp/services/corroboration.py @@ -0,0 +1,52 @@ +"""X11 citation corroboration — classify treatment, match to halacha, aggregate. + +Phase 1: builds the SIGNAL only (no approval changes). See docs/spec/X11. +All LLM calls go through the local claude_session bridge (Opus 4.8 @ xhigh), +same architectural rule as the other extractors (local MCP only). +""" +from __future__ import annotations +import logging +from legal_mcp import config +from legal_mcp.config import parse_llm_json +from legal_mcp.services import claude_session + +logger = logging.getLogger(__name__) + +TREATMENT_POSITIVE = {"followed", "explained"} +TREATMENT_NEGATIVE = {"distinguished", "criticized", "questioned", "overruled"} +TREATMENT_NEUTRAL = {"mentioned"} +_VALID_TREATMENT = TREATMENT_POSITIVE | TREATMENT_NEGATIVE | TREATMENT_NEUTRAL + +def is_positive(t: str) -> bool: return t in TREATMENT_POSITIVE +def is_negative(t: str) -> bool: return t in TREATMENT_NEGATIVE + +def _coerce_treatment(raw: dict) -> str: + t = str((raw or {}).get("treatment", "")).strip().lower() + return t if t in _VALID_TREATMENT else "mentioned" + + +_TREATMENT_PROMPT = """אתה משפטן בכיר. נתון ציטוט של פסק/החלטה קודמים בתוך החלטה מאוחרת. +סווג כיצד ההחלטה המאוחרת **מטפלת** בתקדים המצוטט, לפי אחת מהקטגוריות: +- followed — אימצה והחילה את ההלכה. +- explained — הסבירה/הזכירה בלי לחלוק. +- distinguished — אבחנה (קבעה שלא חל בנסיבות). +- criticized — מתחה ביקורת בלי לבטל. +- questioned — הטילה ספק. +- overruled — דחתה/ביטלה את ההלכה. +- mentioned — אזכור-אגב בלי טיפול. +החזר JSON בלבד: {"treatment": "<קטגוריה>"}. +""" + +async def classify_treatment(cited_citation: str, context: str) -> str: + """Return one treatment label for how `context` treats `cited_citation`.""" + user = f"תקדים מצוטט: {cited_citation}\n\n--- ההקשר המצטט ---\n{context}\n--- סוף ---" + try: + result = await claude_session.query_json( + user, system=_TREATMENT_PROMPT, + model=config.HALACHA_EXTRACT_MODEL or None, + effort=config.HALACHA_EXTRACT_EFFORT or None, + ) + except Exception as e: + logger.warning("classify_treatment failed: %s", e) + return "mentioned" + return _coerce_treatment(result if isinstance(result, dict) else {}) diff --git a/mcp-server/tests/test_corroboration.py b/mcp-server/tests/test_corroboration.py new file mode 100644 index 0000000..07216e7 --- /dev/null +++ b/mcp-server/tests/test_corroboration.py @@ -0,0 +1,17 @@ +from __future__ import annotations +import pytest +from legal_mcp.services import corroboration as cor + +@pytest.mark.parametrize("raw,expected", [ + ({"treatment": "followed"}, "followed"), + ({"treatment": "OVERRULED"}, "overruled"), # case-insensitive + ({"treatment": "bananas"}, "mentioned"), # unknown -> neutral default + ({}, "mentioned"), # missing -> neutral default +]) +def test_coerce_treatment(raw, expected): + assert cor._coerce_treatment(raw) == expected + +def test_treatment_polarity(): + assert cor.is_positive("followed") and cor.is_positive("explained") + assert cor.is_negative("distinguished") and cor.is_negative("overruled") + assert not cor.is_positive("mentioned") and not cor.is_negative("mentioned")