Merge remote-tracking branch 'origin/main' into rebase-358
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

# Conflicts:
#	docs/spec/04-analysis-writing.md
This commit is contained in:
2026-06-30 12:16:41 +00:00
15 changed files with 966 additions and 61 deletions

View File

@@ -264,6 +264,11 @@ async def document_get_text(case_number: str, doc_title: str = "") -> str:
async def document_list(case_number: str) -> str:
"""רשימת מסמכים בתיק.
כל מסמך כולל `doc_type`, וכן את הסיווג הנגזר `is_primary` (bool) ו-
`doc_category` ("primary"/"secondary") — מסמך-עיקרי (ערר/תשובה/התנגדות/
פרוטוקול/שומה/החלטת-ועדה) מול משני. נגזר מ-`doc_type` (db.PRIMARY_DOC_TYPES),
אינו נכתב ידנית.
Args:
case_number: מספר תיק הערר
"""
@@ -345,11 +350,15 @@ async def extract_claims(
if not docs:
return empty(f"אין מסמכים בתיק {case_number}.")
# Filter to claims documents (appeal, response) or specific doc
# Filter to claims documents or a specific doc. protocol carries oral
# arguments raised at the hearing — extracted with claim_type='protocol'
# (claims_extractor._infer_claim_type) so analyze_protocol can compare them
# against the written pleadings, while block ז (write_block_zayin) excludes
# them to stay original-pleadings-only (INV-WR4).
if doc_title:
docs = [d for d in docs if doc_title.lower() in d["title"].lower()]
else:
docs = [d for d in docs if d["doc_type"] in ("appeal", "response", "objection")]
docs = [d for d in docs if d["doc_type"] in ("appeal", "response", "objection", "protocol")]
if not docs:
return empty("לא נמצאו כתבי טענות בתיק.")

View File

@@ -516,9 +516,18 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
# ── Interim draft (pre-ruling) ────────────────────────────────────
# Blocks written for the interim draft, in display order.
# Blocks written for the interim draft, in a FIXED order.
# This is the same content the chair sees in the final decision (same template,
# same skill, same prompts) — minus opening, ruling, summary, signatures.
# same skill, same prompts, same single canonical write path — block_writer) —
# minus ruling, summary, signatures.
#
# Determinism (#204 / WS5): the interim draft is STRUCTURALLY deterministic.
# Each block is generated by block_writer.write_block, which now pins the model
# (Opus 4.8) and a per-block reasoning effort, and uses fixed structural prompts
# (e.g. block-he's opening is always "לפנינו ערר…"). The display order on export
# is fixed separately by docx_exporter._INTERIM_BLOCK_ORDER. There is no parallel
# generation path (G2): write_interim_draft is a thin orchestrator over the same
# write_and_store_block used by the full decision.
_INTERIM_BLOCKS = ["block-he", "block-vav", "block-tet", "block-zayin", "block-chet"]
@@ -571,6 +580,73 @@ async def get_appraiser_facts(case_number: str) -> str:
return err(str(e))
async def analyze_protocol(case_number: str) -> str:
"""ניתוח השוואתי של פרוטוקול-דיון מול כתבי-הטענות (WS4 / #203).
מזהה אילו טענות **ירדו** (נזנחו בדיון), אילו **חוזקו**, ואילו **עלו חדשות**,
מחדד את השאלות המשפטיות, ומחלץ את נתוני הכותרת (א–ד) מהפרוטוקול. התוצאה נכנסת
ל"ידע-התיק" (טבלת protocol_analysis), זמינה לסוכני הניתוח והכתיבה; תאריך-הדיון
מוזן חזרה לעמודה הקנונית cases.hearing_date.
דורש פרוטוקול מתויג doc_type='protocol' + טיעונים מאוגדים
(aggregate_claims_to_arguments). רץ עם Claude מקומי (Opus 4.8, effort=high);
re-run מחליף את הניתוח הקודם לאותו פרוטוקול (idempotent).
Args:
case_number: מספר תיק הערר
"""
from legal_mcp.services import protocol_analyzer
case = await db.get_case_by_number(case_number)
if not case:
return err(f"תיק {case_number} לא נמצא.")
case_id = UUID(case["id"])
try:
result = await protocol_analyzer.analyze_protocol(case_id)
await audit.log_action_safe(
"analyze_protocol", case_id=case_id,
details={"status": result.get("status"), "total": result.get("total", 0)},
)
return ok(result)
except Exception as e:
return err(str(e))
async def get_protocol_analysis(case_number: str, change_type: str = "") -> str:
"""קריאת ניתוח-הפרוטוקול שכבר חולץ — ללא הרצת ניתוח-מחדש (INV-TOOL4).
ה-get המקביל ל-analyze_protocol: מחזיר את רשומות ידע-התיק השמורות
(ירדה/חוזקה/חדשה + שאלה-מחודדת + ציטוט-מבסס), בלי קריאת-LLM יקרה.
מחזיר רשימה ריקה אם הניתוח טרם רץ (status=ok, count=0) — לא שגיאה.
Args:
case_number: מספר תיק הערר
change_type: סינון (dropped/strengthened/newly_raised). ריק = הכל.
"""
from legal_mcp.services import protocol_analyzer
case = await db.get_case_by_number(case_number)
if not case:
return err(f"תיק {case_number} לא נמצא.")
if change_type and change_type not in protocol_analyzer.VALID_CHANGE_TYPES:
return err(
f"change_type לא תקין: {change_type}",
data={"allowed": sorted(protocol_analyzer.VALID_CHANGE_TYPES)},
)
case_id = UUID(case["id"])
try:
rows = await db.list_protocol_analysis(
case_id, change_type=change_type or None,
)
return ok({
"case_number": case_number,
"count": len(rows),
"analysis": rows,
})
except Exception as e:
return err(str(e))
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
"""כתיבת ארבעת הבלוקים לטיוטת ביניים: רקע (ו), תכניות+היתרים (ט),
טענות הצדדים (ז), הליכים (ח). אם לא חולצו עובדות שמאיות עדיין —