"""MCP tools for the missing-precedents log. When a researcher (or chair) finds a citation in a party brief that isn't yet in the precedent_library, they record it here so: 1. The gap is visible in the UI (the chair can see all open citations that need to be uploaded). 2. The writer agent doesn't try to use a precedent that isn't in the corpus — it knows the gap is being tracked. 3. The chair has a clean closing workflow: upload the actual decision via the precedent library / internal-decisions, then link it here. Three tools: - ``missing_precedent_create`` — log a new gap (researcher / chair). - ``missing_precedent_list`` — list open gaps (optionally filtered). - ``missing_precedent_close`` — close a gap (chair workflow). """ from __future__ import annotations from uuid import UUID from legal_mcp.services import db from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope async def _resolve_case_id(case_number: str) -> UUID | None: """Translate a human case_number (e.g. '1017-03-26') to a UUID.""" if not case_number or not case_number.strip(): return None row = await db.get_case_by_number(case_number.strip()) if not row: return None return UUID(row["id"]) async def missing_precedent_create( citation: str, case_number: str = "", cited_in_document_id: str = "", cited_by_party: str = "unknown", cited_by_party_name: str = "", legal_topic: str = "", legal_issue: str = "", claim_quote: str = "", case_name: str = "", notes: str = "", ) -> str: """תיעוד פסיקה שצוטטה אך אינה בקורפוס. הסוכן יוצר רשומה כשהוא מזהה ציטוט שלא ניתן לאמת מול הקורפוס; היו"ר יסגור אותה לאחר העלאת המסמך. Args: citation: מראה המקום המלא (חובה). case_number: מספר תיק הערר שבו צוטטה הפסיקה (לדוגמה '1017-03-26'). cited_in_document_id: UUID של המסמך שבו הציטוט מופיע (אופציונלי). cited_by_party: appellant / respondent / committee / permit_applicant / unknown. cited_by_party_name: שם הצד (כדי שיהיה ברור מי ציטט). legal_topic: נושא משפטי קצר (לדוגמה "זכות עמידה"). legal_issue: שאלה משפטית מפורטת. claim_quote: הציטוט בכתב הטענות. case_name: שם קצר של פסק הדין החסר. notes: הערות חופשיות. Returns: JSON של הרשומה שנוצרה (כולל id) או error. """ if not citation.strip(): return _err("citation חובה") case_id = None if case_number: case_id = await _resolve_case_id(case_number) if case_id is None: return _err(f"תיק לא נמצא: {case_number}") doc_uuid: UUID | None = None if cited_in_document_id.strip(): try: doc_uuid = UUID(cited_in_document_id.strip()) except ValueError: return _err("cited_in_document_id לא תקין") party = cited_by_party.strip() or "unknown" if party not in db.ALLOWED_MP_PARTIES: return _err( f"cited_by_party לא תקין. ערכים תקפים: " f"{', '.join(sorted(db.ALLOWED_MP_PARTIES))}" ) # Deduplication: if a row already exists for the same citation in # the same case, return that one rather than creating a duplicate. existing = await db.find_missing_precedent_by_citation( citation=citation.strip(), case_id=case_id, ) if existing: return _ok({**existing, "_duplicate": True}) try: row = await db.create_missing_precedent( citation=citation.strip(), case_name=case_name.strip() or None, cited_in_case_id=case_id, cited_in_document_id=doc_uuid, cited_by_party=party, cited_by_party_name=cited_by_party_name.strip() or None, legal_topic=legal_topic.strip() or None, legal_issue=legal_issue.strip() or None, claim_quote=claim_quote.strip() or None, notes=notes.strip() or None, ) except Exception as e: return _err(str(e)) return _ok(row) async def missing_precedent_list( case_number: str = "", status: str = "open", legal_topic: str = "", limit: int = 50, ) -> str: """רשימת פסיקות חסרות. ברירת מחדל = פתוחות בלבד. Args: case_number: סינון לפי תיק הערר שבו צוטטו. status: open / uploaded / closed / irrelevant (ריק = הכל). legal_topic: סינון לפי נושא משפטי (substring). limit: מספר תוצאות מקסימלי. Returns: JSON עם רשימת רשומות + linked_case_law_number אם נסגרו. """ case_id = None if case_number: case_id = await _resolve_case_id(case_number) if case_id is None: return _err(f"תיק לא נמצא: {case_number}") s = status.strip() or None if s and s not in db.ALLOWED_MP_STATUS: return _err( f"status לא תקין. ערכים תקפים: " f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}" ) try: rows = await db.list_missing_precedents( status=s, case_id=case_id, legal_topic=legal_topic.strip() or None, limit=max(1, min(int(limit), 500)), ) except Exception as e: return _err(str(e)) return _ok({"items": rows, "count": len(rows)}) async def missing_precedent_close( id: str, linked_case_law_id: str = "", notes: str = "", status: str = "closed", ) -> str: """סגירת רשומת פסיקה חסרה. ברירת מחדל = 'closed' + קישור ל-case_law. Args: id: UUID של הרשומה. linked_case_law_id: UUID של הפסיקה שהועלתה ב-precedent_library / internal_decisions. notes: הערות סגירה (לדוגמה "אינו רלוונטי" ל-status='irrelevant'). status: closed / uploaded / irrelevant. Returns: JSON של הרשומה המעודכנת. """ try: mp_id = UUID(id.strip()) except ValueError: return _err("id לא תקין") cl_uuid: UUID | None = None if linked_case_law_id.strip(): try: cl_uuid = UUID(linked_case_law_id.strip()) except ValueError: return _err("linked_case_law_id לא תקין") status_clean = status.strip() or "closed" if status_clean not in db.ALLOWED_MP_STATUS: return _err( f"status לא תקין. ערכים תקפים: " f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}" ) try: row = await db.close_missing_precedent( mp_id=mp_id, linked_case_law_id=cl_uuid, notes=notes.strip() or None, status=status_clean, ) except Exception as e: return _err(str(e)) if row is None: return _err("רשומה לא נמצאה") return _ok(row)