מסמך פרוזה נפרד מטיוטת-הביניים (החלטת-יו"ר): תמצית מוקפדת של טענות+תשובות
להכנת היו"ר לדיון. נגזר מ-legal_arguments/claims הקיימים — אותו מקור-אמת,
ללא חילוץ-מחדש (G2). מתוחם לטענות-התיק בלבד (INV-AH).
- כלי-MCP חדש summarize_party_claims (drafting.py + server.py); ייצור עובר
claude_session נעוץ Opus 4.8 + effort=high (זיקוק/סינתזה); local-only.
- שירות party_claims_summary.py: בחירת-מקור arguments→claims, פרומפט מעוגן-מקור,
שמירה ל-data/cases/{n}/documents/research/ (git + S3).
- ייצוא DOCX בסגנון-תבנית דפנה (build_party_claims_summary_docx) + endpoints
read/download/export-docx ב-web/app.py (מגישים בלבד — אין claude CLI בקונטיינר).
- _next_version הוכלל ל-prefix-scoped (תאימות-לאחור) למניעת התנגשות-גרסאות.
- ספ 04-analysis-writing.md §1.3 + 8 בדיקות אופליין (מקור/נעיצת-מודל/markdown→docx).
Invariants: G2 (מקור-אמת יחיד, בלי מסלול מקביל) · X9 TOOL1/TOOL4 (envelope +
extract↔serve) · INV-AH (עיגון-מקור, abstention על טענה לא-ברורה) ·
claude_session local-only · G9 (provenance — נשמר durably + git/S3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
213 lines
7.9 KiB
Python
213 lines
7.9 KiB
Python
"""Tests for the party-claims executive summary (#202, WS3).
|
|
|
|
Cover the deterministic, offline parts:
|
|
- ``_build_arguments_context`` source selection: legal_arguments preferred,
|
|
raw claims fallback, "none" when empty (G2 — single source, no re-extract).
|
|
- ``summarize_party_claims`` pins Opus 4.8 + effort=high and grounds the
|
|
prompt strictly in the case's own claims (anti-hallucination / INV-AH).
|
|
- ``_render_markdown_body`` maps markdown headings/bullets onto template styles.
|
|
|
|
Generation itself (claude -p) is local-only and not exercised here — the
|
|
claude_session call is monkeypatched.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from uuid import uuid4
|
|
|
|
from legal_mcp.services import party_claims_summary as pcs
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
# ── source selection (G2: arguments preferred, claims fallback) ────
|
|
|
|
|
|
def test_arguments_preferred_over_claims(monkeypatch):
|
|
cid = uuid4()
|
|
|
|
async def _get_args(_case_id):
|
|
return [
|
|
{"party": "appellant", "argument_title": "חריגת בנייה",
|
|
"argument_body": "הבנייה חורגת מקו הבניין", "legal_topic": "קווי בניין"},
|
|
{"party": "respondent", "argument_title": "התיישנות",
|
|
"argument_body": "", "legal_topic": ""},
|
|
]
|
|
|
|
async def _get_claims(_case_id, *a, **k): # must NOT be consulted
|
|
raise AssertionError("claims fallback used despite arguments existing")
|
|
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _get_args)
|
|
monkeypatch.setattr(pcs.db, "get_claims", _get_claims)
|
|
|
|
ctx, n, kind = _run(pcs._build_arguments_context(cid))
|
|
assert kind == "arguments"
|
|
assert n == 2
|
|
assert "העוררים" in ctx and "המשיבים" in ctx
|
|
assert "חריגת בנייה" in ctx
|
|
|
|
|
|
def test_falls_back_to_claims_when_no_arguments(monkeypatch):
|
|
cid = uuid4()
|
|
|
|
async def _no_args(_case_id):
|
|
return []
|
|
|
|
async def _claims(_case_id, *a, **k):
|
|
return [
|
|
{"party_role": "appellant", "claim_text": "טענה אחת", "source_document": "appeal"},
|
|
{"party_role": "respondent", "claim_text": "טענה שתיים", "source_document": "response"},
|
|
]
|
|
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _no_args)
|
|
monkeypatch.setattr(pcs.db, "get_claims", _claims)
|
|
|
|
ctx, n, kind = _run(pcs._build_arguments_context(cid))
|
|
assert kind == "claims"
|
|
assert n == 2
|
|
assert "טענה אחת" in ctx
|
|
|
|
|
|
def test_block_zayin_claims_excluded(monkeypatch):
|
|
"""Decision-summary (block-zayin) claims are not original pleadings."""
|
|
cid = uuid4()
|
|
|
|
async def _no_args(_case_id):
|
|
return []
|
|
|
|
async def _claims(_case_id, *a, **k):
|
|
return [
|
|
{"party_role": "appellant", "claim_text": "מקורית", "source_document": "appeal"},
|
|
{"party_role": "appellant", "claim_text": "מסיכום", "source_document": "block-zayin"},
|
|
]
|
|
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _no_args)
|
|
monkeypatch.setattr(pcs.db, "get_claims", _claims)
|
|
|
|
ctx, n, kind = _run(pcs._build_arguments_context(cid))
|
|
assert kind == "claims"
|
|
assert n == 1
|
|
assert "מקורית" in ctx and "מסיכום" not in ctx
|
|
|
|
|
|
def test_none_when_no_source(monkeypatch):
|
|
cid = uuid4()
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments",
|
|
lambda _c: _aw([]))
|
|
monkeypatch.setattr(pcs.db, "get_claims", lambda _c, *a, **k: _aw([]))
|
|
ctx, n, kind = _run(pcs._build_arguments_context(cid))
|
|
assert kind == "none" and n == 0 and ctx == ""
|
|
|
|
|
|
async def _aw(v):
|
|
return v
|
|
|
|
|
|
# ── generation pins Opus 4.8 + high effort, grounded in claims ─────
|
|
|
|
|
|
def test_summarize_pins_opus_and_high_effort(monkeypatch, tmp_path):
|
|
cid = uuid4()
|
|
captured: dict = {}
|
|
|
|
async def _get_case(_case_id):
|
|
return {"case_number": "8125-09-24", "title": "t", "appellants": ["א"],
|
|
"respondents": ["ב"], "subject": "היטל השבחה", "property_address": "",
|
|
"appeal_type": "betterment_levy", "practice_area": "betterment_levy"}
|
|
|
|
async def _get_decision(_case_id):
|
|
return None
|
|
|
|
async def _args(_case_id):
|
|
return [{"party": "appellant", "argument_title": "X",
|
|
"argument_body": "Y", "legal_topic": ""}]
|
|
|
|
async def _fake_query(prompt, *, timeout=None, model=None, effort=None, tools=None):
|
|
captured["model"] = model
|
|
captured["effort"] = effort
|
|
captured["tools"] = tools
|
|
captured["prompt"] = prompt
|
|
return "## טענות העוררים\nתמצית."
|
|
|
|
async def _style(_practice_area=""):
|
|
return "(style)"
|
|
|
|
monkeypatch.setattr(pcs.db, "get_case", _get_case)
|
|
monkeypatch.setattr(pcs.db, "get_decision_by_case", _get_decision)
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", _args)
|
|
monkeypatch.setattr(pcs.block_writer, "_build_style_context", _style)
|
|
monkeypatch.setattr(pcs.claude_session, "query", _fake_query)
|
|
monkeypatch.setattr(pcs.config, "find_case_dir", lambda cn: tmp_path / cn)
|
|
monkeypatch.setattr(pcs.config, "DATA_DIR", tmp_path)
|
|
|
|
async def _mirror(*a, **k):
|
|
return None
|
|
monkeypatch.setattr(pcs.storage, "mirror", _mirror)
|
|
monkeypatch.setattr(pcs.git_sync, "commit_and_push", lambda *a, **k: True)
|
|
|
|
result = _run(pcs.summarize_party_claims(cid, "8125-09-24"))
|
|
|
|
# The whole point of #202's generation-path constraint:
|
|
assert captured["model"] == "claude-opus-4-8"
|
|
assert captured["effort"] == "high"
|
|
assert captured["tools"] == "" # prose, no tool_use
|
|
# Grounded strictly in the case's own arguments (anti-hallucination).
|
|
assert "אנטי-הזיה" in captured["prompt"]
|
|
assert "Y" in captured["prompt"] # the actual argument body is in-context
|
|
|
|
assert result["source_kind"] == "arguments"
|
|
out = (tmp_path / "8125-09-24" / "documents" / "research" / "party-claims-summary.md")
|
|
assert out.exists()
|
|
assert "סיכום-מנהלים" in out.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_summarize_raises_when_no_claims(monkeypatch, tmp_path):
|
|
cid = uuid4()
|
|
monkeypatch.setattr(pcs.db, "get_case", lambda _c: _aw({"case_number": "9000-01-25"}))
|
|
monkeypatch.setattr(pcs.db, "get_decision_by_case", lambda _c: _aw(None))
|
|
monkeypatch.setattr(pcs.argument_aggregator, "get_legal_arguments", lambda _c: _aw([]))
|
|
monkeypatch.setattr(pcs.db, "get_claims", lambda _c, *a, **k: _aw([]))
|
|
|
|
try:
|
|
_run(pcs.summarize_party_claims(cid, "9000-01-25"))
|
|
raise AssertionError("expected ValueError on empty source")
|
|
except ValueError as e:
|
|
assert "טענות" in str(e)
|
|
|
|
|
|
# ── markdown → DOCX rendering uses template heading styles ─────────
|
|
|
|
|
|
def test_render_markdown_body_maps_styles():
|
|
from docx import Document
|
|
from legal_mcp.services import analysis_docx_exporter as ax
|
|
|
|
doc = Document()
|
|
md = (
|
|
"# כותרת ראשית\n\n"
|
|
"---\n"
|
|
"## טענות העוררים\n"
|
|
"- טענה אחת\n"
|
|
"1. סעיף ממוספר\n"
|
|
"טקסט רגיל.\n"
|
|
)
|
|
ax._render_markdown_body(doc, md)
|
|
styles = [p.style.name for p in doc.paragraphs]
|
|
texts = [p.text for p in doc.paragraphs]
|
|
# HR is skipped; headings become Heading styles.
|
|
assert "כותרת ראשית" in texts
|
|
assert "Heading 1" in styles # # / ## → Heading 1
|
|
assert any("טענה אחת" in t for t in texts)
|
|
assert not any(t.strip() in {"---", "#"} for t in texts)
|
|
|
|
|
|
def test_next_version_is_prefix_scoped(tmp_path):
|
|
from legal_mcp.services import analysis_docx_exporter as ax
|
|
(tmp_path / "ניתוח-משפטי-v3.docx").write_bytes(b"x")
|
|
# An analysis file at v3 must NOT bump the summary's version.
|
|
assert ax._next_version(tmp_path, "סיכום-מנהלים-טענות") == 1
|
|
assert ax._next_version(tmp_path, "ניתוח-משפטי") == 4
|