From 33f955e372a9874f705508ce60389f0b4e7c848a Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 31 May 2026 19:00:16 +0000 Subject: [PATCH] =?UTF-8?q?feat(corroboration):=20aggregator=20=E2=80=94?= =?UTF-8?q?=20distinct=20positive=20+=20negative-flag=20(INV-COR4,=20X11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../src/legal_mcp/services/corroboration.py | 17 ++++++++++++++++ mcp-server/tests/test_corroboration.py | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/mcp-server/src/legal_mcp/services/corroboration.py b/mcp-server/src/legal_mcp/services/corroboration.py index 089d68b..9cfec9d 100644 --- a/mcp-server/src/legal_mcp/services/corroboration.py +++ b/mcp-server/src/legal_mcp/services/corroboration.py @@ -33,6 +33,23 @@ def accept_match(best: tuple[str, float] | None, floor: float = config.HALACHA_C return halacha_id if sim >= floor else None +def aggregate(links: list[dict], min_cites: int = config.HALACHA_CORROBORATION_MIN_CITES) -> dict: + """Aggregate per-halacha corroboration (INV-COR4/COR2). + + links: [{"source_id": str, "treatment": str}, ...] already matched to ONE halacha. + positive_sources = count of DISTINCT source_id whose treatment is positive. + has_negative = any negative treatment present. + corroborated = positive_sources >= min_cites AND not has_negative. + """ + positive = {l["source_id"] for l in links if is_positive(l["treatment"])} + has_negative = any(is_negative(l["treatment"]) for l in links) + return { + "positive_sources": len(positive), + "has_negative": has_negative, + "corroborated": len(positive) >= min_cites and not has_negative, + } + + _TREATMENT_PROMPT = """אתה משפטן בכיר. נתון ציטוט של פסק/החלטה קודמים בתוך החלטה מאוחרת. סווג כיצד ההחלטה המאוחרת **מטפלת** בתקדים המצוטט, לפי אחת מהקטגוריות: - followed — אימצה והחילה את ההלכה. diff --git a/mcp-server/tests/test_corroboration.py b/mcp-server/tests/test_corroboration.py index 0aff981..0ef33dd 100644 --- a/mcp-server/tests/test_corroboration.py +++ b/mcp-server/tests/test_corroboration.py @@ -24,3 +24,23 @@ def test_match_rejects_below_threshold(): def test_match_rejects_empty(): assert cor.accept_match(None, floor=0.50) is None + +def _link(src, treatment): + return {"source_id": src, "treatment": treatment} + +def test_aggregate_counts_distinct_positive(): + links = [_link("d1","followed"), _link("d1","explained"), _link("d2","followed")] + agg = cor.aggregate(links, min_cites=2) + assert agg["positive_sources"] == 2 # d1 counted once (INV-COR4 independence) + assert agg["has_negative"] is False + assert agg["corroborated"] is True + +def test_aggregate_negative_blocks(): + links = [_link("d1","followed"), _link("d2","followed"), _link("d3","distinguished")] + agg = cor.aggregate(links, min_cites=2) + assert agg["has_negative"] is True + assert agg["corroborated"] is False # any negative -> not corroborated (INV-COR2) + +def test_aggregate_below_threshold(): + agg = cor.aggregate([_link("d1","followed")], min_cites=2) + assert agg["corroborated"] is False # single source insufficient (INV-COR4)