Files
legal-ai/mcp-server/src/legal_mcp/tools/plans.py
Chaim 4be9cf8543
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(plans): מרשם-תכניות קנוני (V38) + נוסח-ציטוט אחיד דטרמיניסטי לבלוק ט
מוסיף ישות קנונית לתכניות בניין-עיר (תב"ע) שחוזרות בין תיקים — 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>
2026-06-14 13:46:26 +00:00

139 lines
5.6 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_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)