"""MCP tools for attached legal precedents (user-supplied case-law quotes). These complement the existing `case_law` table (which is populated from structured sources and is what the block-writer RAG searches) by storing free-text citations the chair attaches during the compose phase. """ from __future__ import annotations from uuid import UUID from legal_mcp.services import db from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope async def precedent_attach( case_number: str, quote: str, citation: str, section_id: str = "", chair_note: str = "", pdf_document_id: str = "", ) -> str: """צירוף פסיקה תומכת לתיק ערר. Args: case_number: מספר תיק הערר quote: הציטוט המדויק שיוכנס להחלטה citation: מראה המקום (ערר 1126-08-25 ... נ' ... (נבו 9.3.2026)) section_id: מזהה הטענה/סוגיה (threshold_1, issue_3); ריק = כללי לתיק chair_note: הערה אופציונלית — למה הציטוט תומך בעמדה pdf_document_id: מזהה קובץ PDF מצורף (אופציונלי) """ case = await db.get_case_by_number(case_number) if not case: return err(f"תיק {case_number} לא נמצא.") pdf_uuid: UUID | None = None if pdf_document_id: try: pdf_uuid = UUID(pdf_document_id) except ValueError: return err("pdf_document_id לא תקין") # INV-TOOL3 / GAP-52: idempotent on (case_id, section_id, citation, quote). # Re-attaching the same quote to the same section returns the existing row. for _p in await db.list_case_precedents(UUID(case["id"])): if (_p.get("citation") == citation and _p.get("quote") == quote and (_p.get("section_id") or None) == (section_id or None)): _p["idempotent_existing"] = True return ok(_p) row = await db.create_case_precedent( case_id=UUID(case["id"]), quote=quote, citation=citation, section_id=section_id or None, chair_note=chair_note, pdf_document_id=pdf_uuid, practice_area=case.get("practice_area"), ) return ok(row) async def precedent_list(case_number: str) -> str: """רשימת כל הפסיקות שצורפו לתיק, ממוינות לפי סעיף ואז לפי זמן יצירה.""" case = await db.get_case_by_number(case_number) if not case: return err(f"תיק {case_number} לא נמצא.") rows = await db.list_case_precedents(UUID(case["id"])) return ok(rows) async def precedent_remove(precedent_id: str) -> str: """הסרת פסיקה מצורפת. קובץ ה-PDF (אם צורף) נשאר ב-documents לצורך audit.""" try: pid = UUID(precedent_id) except ValueError: return err("precedent_id לא תקין") deleted = await db.delete_case_precedent(pid) return ok({"deleted": deleted, "precedent_id": precedent_id}) async def search_case_precedents( query: str, practice_area: str = "", limit: int = 10, ) -> str: """חיפוש רוחבי בציטוטי-הפסיקה שצורפו ידנית לתיקים (case_precedents) — קורפוס "case-attached". GAP-49 (INV-TOOL2): שם קודם `precedent_search_library` (מטעה). זו **אינה** ספריית-הפסיקה הסמכותית — לזו השתמש ב-`search_precedent_library`. Args: query: מחרוזת חיפוש (מתחרה מול citation ומול quote) practice_area: אופציונלי — סינון לתחום משפטי מסוים limit: מספר תוצאות מקסימלי """ if not query or len(query.strip()) < 2: return empty("שאילתה קצרה מדי (פחות מ-2 תווים).") rows = await db.search_precedent_library(query.strip(), practice_area, limit) return ok(rows)