"""MCP tools for the planning-schemes registry (טבלת plans, V38). Extract/get-symmetry per X9: `extract_plans` is the expensive LLM extraction; `plan_get`/`plan_search`/`plan_list` are cheap reads. `plan_upsert` is the manual chair-curated write, `plan_review` is the human approval gate (INV-DM5/G10). Every tool returns the SSoT JSON envelope (ok/err) — GAP-48. """ from __future__ import annotations from uuid import UUID from legal_mcp.services import db from legal_mcp.tools.envelope import err, ok _VALID_STATUS = ("pending_review", "approved", "rejected") async def extract_plans(case_number: str) -> str: """חילוץ תכניות ותוקפן מכל מסמכי התיק אל מרשם-התכניות (נכנסות pending_review). ה-extract היקר (קריאת-LLM, לא-דטרמיניסטי) שמזין את המרשם. הרשומות ממתינות לאישור-יו"ר (plan_review) לפני שישמשו בכתיבת בלוק ט. Args: case_number: מספר תיק הערר """ from legal_mcp.services import plans_extractor case = await db.get_case_by_number(case_number) if not case: return err(f"תיק {case_number} לא נמצא.") try: result = await plans_extractor.extract_plans_for_case(UUID(case["id"])) return ok(result) except Exception as e: # noqa: BLE001 — surface, don't swallow return err(str(e)) async def plan_get(plan_number: str) -> str: """קריאת תכנית מהמרשם לפי מספר (מנורמל; נופל ל-alias). ה-get הזול.""" try: plan = await db.get_plan_by_number(plan_number) except Exception as e: # noqa: BLE001 return err(str(e)) if not plan: return err(f"תכנית '{plan_number}' לא נמצאה במרשם.") return ok(plan) async def plan_search(query: str, limit: int = 20) -> str: """חיפוש fuzzy במרשם לפי מספר/שם/ייעוד (ILIKE + tsvector).""" try: results = await db.search_plans(query, limit) return ok({"query": query, "count": len(results), "results": results}) except Exception as e: # noqa: BLE001 return err(str(e)) async def plan_list(review_status: str = "", limit: int = 500) -> str: """רשימת תכניות במרשם, אופציונלית מסוננת לפי review_status (תור-אישור).""" if review_status and review_status not in _VALID_STATUS: return err("review_status חייב להיות אחד מ: pending_review / approved / rejected") try: plans = await db.list_plans(review_status, limit) return ok({ "count": len(plans), "review_status": review_status or "all", "plans": plans, }) except Exception as e: # noqa: BLE001 return err(str(e)) async def plan_upsert( plan_number: str, display_name: str = "", plan_type: str = "", gazette_date: str = "", yalkut_number: str = "", purpose: str = "", review_status: str = "approved", aliases: str = "", ) -> str: """כתיבה/עריכה ידנית מבוקרת של תכנית במרשם. קלט-יו"ר ידני — ברירת-המחדל review_status='approved' (היו"ר היא הסמכות). מנרמל את plan_number בכתיבה (G1) ו-upsert idempotent (G3). אם התכנית כבר מאושרת ותוקף-חדש סותר — הנתון נרשם ב-discrepancies ולא דורס (G10/§6). Args: plan_number: מזהה-התכנית (יומר לצורה קנונית) — למשל "מי/820", "תמ\\"א 38" display_name: שם-תצוגה כולל "תכנית" (למשל "תכנית מי/820") plan_type: ארצית/מחוזית/מקומית/מפורטת/כוללנית (אופציונלי) gazette_date: תאריך פרסום למתן תוקף ברשומות, ISO YYYY-MM-DD (אופציונלי) yalkut_number: מס' ילקוט הפרסומים / י"פ (אופציונלי) purpose: משפט-ייעוד אחד (אופציונלי) review_status: pending_review/approved/rejected (ברירת-מחדל approved) aliases: צורות חלופיות מופרדות בפסיק (אופציונלי) """ if review_status not in _VALID_STATUS: return err("review_status חייב להיות אחד מ: pending_review / approved / rejected") alias_list = [a.strip() for a in aliases.split(",") if a.strip()] if aliases else [] try: plan = await db.upsert_plan( plan_number=plan_number, display_name=display_name, aliases=alias_list, plan_type=plan_type, gazette_date=gazette_date or None, yalkut_number=yalkut_number, purpose=purpose, review_status=review_status, model_used="chair_manual", ) return ok(plan) except ValueError as e: return err(str(e)) except Exception as e: # noqa: BLE001 return err(str(e)) async def plan_review(plan_id: str, status: str) -> str: """שער-היו"ר (G10): אישור / דחייה / איפוס של רשומת-תכנית במרשם. Args: plan_id: מזהה ה-UUID של הרשומה (מ-plan_list/plan_get) status: approved / rejected / pending_review """ if status not in _VALID_STATUS: return err("status חייב להיות approved / rejected / pending_review") try: plan = await db.set_plan_review_status(UUID(plan_id), status) except Exception as e: # noqa: BLE001 return err(str(e)) if not plan: return err(f"תכנית {plan_id} לא נמצאה.") return ok(plan)