מוסיף ישות קנונית לתכניות בניין-עיר (תב"ע) שחוזרות בין תיקים — SSOT לזהות+תוקף (פרסום למתן תוקף ברשומות + מס' ילקוט-הפרסומים) + משפט-ייעוד — במקום גזירה-מחדש מהשומות בכל תיק. בלוק ט מצטט את התוקף בנוסח אחיד דטרמיניסטי (format_plan_citation), כך שתאריך-פרסום/מס'-ילקוט לעולם לא מהוזים ע"י ה-LLM. - DB: טבלת plans (V38) + CRUD + _normalize_plan_number (G1) + format_plan_citation; upsert idempotent (G3) עם כלל-מיזוג: תוקף מאושר לא נדרס — סתירה נרשמת ב-discrepancies (G10 / אין בליעה שקטה). - services/plans_extractor.py: חילוץ עובדתי (claude CLI מקומי) → pending_review. - block_writer.py: _build_plans_registry_context מזריק משפטי-ציטוט מאושרים בלבד לבלוק ט; תכניות חסרות/לא-מאושרות מסומנות במפורש (לא נבלעות). - tools/plans.py + server.py: extract_plans / plan_get / plan_search / plan_list / plan_upsert / plan_review (שער-יו"ר G10), עם extract/get-symmetry (X9). - scripts/backfill_plans_registry.py: ייבוא מקורפוס-ההחלטות (טיוטות + סופיי-דפנה). - docs: block-schema (בלוק ט), SKILL, spec 02-data-model + 04. Invariants: G1/INV-DM2/X1 (מזהה מנורמל בכתיבה) · G2/INV-DM6 (מקור-אמת יחיד, appraiser_facts ללא שינוי) · G3 (upsert) · INV-DM4/G9 (provenance) · INV-DM5/G10 (review_status) · INV-AH (ציטוט דטרמיניסטי) · G5 (lookup לא קורפוס) · G11/block-schema (נוסח-הציטוט) · X9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.6 KiB
Python
139 lines
5.6 KiB
Python
"""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)
|