Files
legal-ai/mcp-server/src/legal_mcp/tools/plans.py
Chaim a55ffd59eb
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(plans): משיכת תב"ע מ-מנהל-התכנון (mavat) — Phase C backend-slice
ליבת-המשיכה למרשם-התכניות (V38): מספר-תכנית → זהות+תוקף מ-mavat דרך
גשר-Camoufox הקיים (G2 — אותו שירות/פורט/סוד כמו X13, בלי חדשים).

- court_fetch_service/mavat_client.py (חדש): דרייבר Camoufox מול mavat —
  עוקף F5-ASM (דפדפן-JS), search→auto-nav ל-SV4, לוכד GET /rest/api/SV4/1,
  מפענח planDetails (E_NAME/AUTH/ENTITY_SUBTYPE/GOALS) + rsInternet
  (פרסום-לאישור→ED_PUBLICATION_FILE=י"פ + DETAILS→תאריך/עמוד). מלכודת-
  דרייבר: init-script window.onerror swallow. reCAPTCHA נשאר דלוק (token).
- court_fetch_service/server.py: POST /plan-fetch (אותו Bearer).
- services/plans_fetch.py (חדש): צד-קונטיינר — httpx לגשר, מנרמל שדות.
- tools/plans.py + server.py: כלי-MCP plan_fetch (מועמד, לא כותב).
- web/app.py: POST /api/plans/fetch (503 גשר-למטה, 404 לא-נמצא).

אומת חי מול mavat: 101-1031020→י"פ 13697 (עמ' 8758, 30/07/2025),
101-1053933→י"פ 13836. מקור-אמת עשיר מתב"ע-עכשיו (שחסר י"פ).

INV-AH: כל ערך נושא source_url; שדה-חסר ריק לא מומצא. G10: מחזיר
מועמד בלבד — שער-יו"ר (review_status) נשמר. G2: מרחיב גשר+מרשם קיימים.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:09:39 +00:00

161 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_fetch(plan_number: str) -> str:
"""משיכת זהות+תוקף של תב"ע מ-מנהל-התכנון (mavat) — מועמד-לאישור, לא כתיבה.
מחזיר את שדות-התכנית כפי שנמשכו (display_name/plan_type/purpose/gazette_date/
yalkut_number/source_url). כל ערך נושא source_url (INV-AH); שדה שהמקור אינו
חושף חוזר ריק — לא מומצא. אינו כותב למרשם: הקורא (טופס-היו"ר / plan_upsert)
מחליט; הרשומה תמיד עוברת שער-יו"ר (review_status) לפני ציטוט בבלוק ט.
Args:
plan_number: מספר-התכנית למשיכה (למשל "101-1031020", "מי/820")
"""
from legal_mcp.services import plans_fetch
try:
plan = await plans_fetch.fetch_plan(plan_number)
return ok(plan)
except plans_fetch.PlanFetchUnavailable as e:
return err(f"שירות-המשיכה אינו זמין: {e}")
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)