feat(analysis): analyze_protocol — comparative hearing-protocol analysis (WS4 #203)
מנתח פרוטוקול-דיון מול כתבי-הטענות: אילו טענות ירדו/חוזקו/עלו-חדשות + חידוד שאלות משפטיות, ומחלץ נתוני כותרת (א–ד). התוצאה נכנסת לידע-התיק. - protocol_analyzer.py — ניתוח השוואתי דרך claude_session (Opus 4.8, effort=high, מקומי בלבד); שער anti-hallucination (quote-or-retract) ב-_normalize_change; חילוץ א–ד + הזנת hearing_date חזרה ל-cases (לא דורס קלט-יו"ר). - claims_extractor: protocol → claim_type='protocol'; extract_claims קולט protocol. - block_writer._build_claims_context מחריג claim_type='protocol' מבלוק ז (INV-WR4). - db: טבלת protocol_analysis (V47, idempotent per-doc) + replace/list helpers. - tools/drafting + server: analyze_protocol + get_protocol_analysis (extract/get). - ספ 04-analysis-writing §1.3 + 15 בדיקות. Invariants: G1 (נרמול-במקור: hearing_date לעמודה הקנונית), G2 (ידע-תיק נגזר ממקור-האמת protocol+legal_arguments, לא מסלול מקביל), INV-TOOL3 (idempotent per document_id), INV-TOOL4 (extract/get symmetry), INV-AH (quote-or-retract), INV-WR4 (בלוק ז = טענות-כתב מקוריות בלבד), claude_session local-only, G12 (נקי מ-Paperclip — leak-guard עובר). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
141
mcp-server/tests/test_protocol_analyzer.py
Normal file
141
mcp-server/tests/test_protocol_analyzer.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Tests for the comparative protocol analyzer (WS4 / #203).
|
||||
|
||||
Covers the pure (non-LLM, non-DB) logic: the anti-hallucination normalization
|
||||
gate, protocol-document discovery, claim_type tagging for protocols, and the
|
||||
block-ז filter that keeps oral hearing arguments out of the original-pleadings
|
||||
summary (INV-WR4).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from legal_mcp.services import protocol_analyzer as pa
|
||||
from legal_mcp.services.claims_extractor import _infer_claim_type
|
||||
|
||||
|
||||
# ── claim_type tagging — protocol distinct from written pleadings ───────────
|
||||
|
||||
@pytest.mark.parametrize("doc_type,title,expected", [
|
||||
("protocol", "פרוטוקול דיון", "protocol"),
|
||||
("appeal", "פרוטוקול הדיון מיום 1.1", "protocol"), # title-based
|
||||
("appeal", "כתב ערר", "claim"),
|
||||
("response", "כתב תשובה", "response"),
|
||||
("response", "תגובת המשיבה", "reply"),
|
||||
])
|
||||
def test_infer_claim_type(doc_type, title, expected):
|
||||
assert _infer_claim_type(doc_type, title) == expected
|
||||
|
||||
|
||||
# ── _find_protocol ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_find_protocol_by_doc_type():
|
||||
docs = [
|
||||
{"id": "1", "doc_type": "appeal", "title": "כתב ערר"},
|
||||
{"id": "2", "doc_type": "protocol", "title": "פרוטוקול"},
|
||||
]
|
||||
assert pa._find_protocol(docs)["id"] == "2"
|
||||
|
||||
|
||||
def test_find_protocol_by_title_fallback():
|
||||
docs = [
|
||||
{"id": "1", "doc_type": "reference", "title": "פרוטוקול הדיון"},
|
||||
{"id": "2", "doc_type": "appeal", "title": "כתב ערר"},
|
||||
]
|
||||
assert pa._find_protocol(docs)["id"] == "1"
|
||||
|
||||
|
||||
def test_find_protocol_none():
|
||||
docs = [{"id": "1", "doc_type": "appeal", "title": "כתב ערר"}]
|
||||
assert pa._find_protocol(docs) is None
|
||||
|
||||
|
||||
# ── _normalize_change — anti-hallucination gate (INV-AH) ───────────────────
|
||||
|
||||
_AID = "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
|
||||
def test_normalize_change_valid():
|
||||
row = pa._normalize_change(
|
||||
{
|
||||
"change_type": "dropped",
|
||||
"party_role": "appellant",
|
||||
"argument_id": _AID,
|
||||
"argument_title": "טענת השיהוי",
|
||||
"summary": "העורר ויתר על הטענה בדיון.",
|
||||
"sharpened_question": "האם נותרה טענת סף?",
|
||||
"evidence_quote": "ב\"כ העורר: איננו עומדים על טענת השיהוי.",
|
||||
"page_number": 3,
|
||||
},
|
||||
valid_argument_ids={_AID},
|
||||
)
|
||||
assert row is not None
|
||||
assert row["change_type"] == "dropped"
|
||||
assert str(row["argument_id"]) == _AID
|
||||
assert row["page_number"] == 3
|
||||
|
||||
|
||||
def test_normalize_change_drops_row_without_evidence_quote():
|
||||
# quote-or-retract: no verbatim support → row is rejected at source.
|
||||
row = pa._normalize_change(
|
||||
{"change_type": "strengthened", "summary": "חוזקה", "evidence_quote": ""},
|
||||
valid_argument_ids=set(),
|
||||
)
|
||||
assert row is None
|
||||
|
||||
|
||||
def test_normalize_change_drops_row_without_summary():
|
||||
row = pa._normalize_change(
|
||||
{"change_type": "dropped", "summary": "", "evidence_quote": "ציטוט"},
|
||||
valid_argument_ids=set(),
|
||||
)
|
||||
assert row is None
|
||||
|
||||
|
||||
def test_normalize_change_rejects_bad_change_type():
|
||||
row = pa._normalize_change(
|
||||
{"change_type": "modified", "summary": "x", "evidence_quote": "y"},
|
||||
valid_argument_ids=set(),
|
||||
)
|
||||
assert row is None
|
||||
|
||||
|
||||
def test_normalize_change_drops_hallucinated_argument_id():
|
||||
# An argument_id that isn't one of this case's arguments must be dropped
|
||||
# (FK safety) — but the row itself, being newly_raised-shaped, survives.
|
||||
row = pa._normalize_change(
|
||||
{
|
||||
"change_type": "newly_raised",
|
||||
"party_role": "respondent",
|
||||
"argument_id": "99999999-9999-9999-9999-999999999999",
|
||||
"summary": "סוגיה חדשה שעלתה בדיון.",
|
||||
"evidence_quote": "ב\"כ המשיבה העלה לראשונה את שאלת הסמכות.",
|
||||
},
|
||||
valid_argument_ids={_AID},
|
||||
)
|
||||
assert row is not None
|
||||
assert row["argument_id"] is None
|
||||
assert row["change_type"] == "newly_raised"
|
||||
|
||||
|
||||
def test_normalize_change_invalid_party_role_blanked():
|
||||
row = pa._normalize_change(
|
||||
{
|
||||
"change_type": "strengthened",
|
||||
"party_role": "judge", # not a valid party
|
||||
"summary": "חוזקה הטענה.",
|
||||
"evidence_quote": "ציטוט מבסס.",
|
||||
},
|
||||
valid_argument_ids=set(),
|
||||
)
|
||||
assert row is not None
|
||||
assert row["party_role"] == ""
|
||||
|
||||
|
||||
def test_normalize_change_truncates_quote():
|
||||
long_quote = "א" * 500
|
||||
row = pa._normalize_change(
|
||||
{"change_type": "dropped", "summary": "x", "evidence_quote": long_quote},
|
||||
valid_argument_ids=set(),
|
||||
)
|
||||
assert row is not None
|
||||
assert len(row["evidence_quote"]) == 200
|
||||
Reference in New Issue
Block a user