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>
This commit is contained in:
138
mcp-server/src/legal_mcp/tools/plans.py
Normal file
138
mcp-server/src/legal_mcp/tools/plans.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user