feat(mcp): FU-14 GAP-48 פרוסה 2 — envelope אחיד ל-11 משפחות-כלים

המשך מיגרציית INV-TOOL1 מעבר למשפחת-החיפוש (#71). הומרו ל-{status,data,message}:
precedent_library, citations, internal_decisions, missing_precedents,
training_enrichment, precedents, legal_arguments, cases, documents, workflow
(~55 כלים). בוטלו 5 עותקי _ok/_err משוכפלים (alias ל-tools/envelope.py — SSoT, G2).

עיקרון: envelope-status = הצלחת-הקריאה-לכלי; תוצאה-עסקית (idempotent_existing,
noop, completed...) נשמרת בתוך data. err רק לכשל אמיתי (not-found/invalid/exception).

תאימות-API: צרכני web/app.py של cases/workflow/precedents חוּוטו דרך
envelope_unwrap + בדיקת status=="error"→4xx — תשובת ה-HTTP זהה, web-ui לא מושפע.
(documents/legal_arguments/citations/... אינם נצרכים מ-app.py — agent-only.)

בדיקות: 182/182 עוברים (test_corpus_constraints עודכן לחוזה החדש).
נותר: משפחת drafting (מסלול הפקת-ההחלטה) בפרוסה נפרדת עם שער טסט-ייצוא.

Invariants: מקדם INV-TOOL1 + G2 (SSoT, ביטול כפילות). מתועד ב-X9 + gap-audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 17:41:39 +00:00
parent 24f9ceb164
commit 79b9c37301
14 changed files with 168 additions and 240 deletions

View File

@@ -7,11 +7,10 @@ free-text citations the chair attaches during the compose phase.
from __future__ import annotations
import json
from pathlib import Path
from uuid import UUID
from legal_mcp.services import db
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
async def precedent_attach(
@@ -34,14 +33,14 @@ async def precedent_attach(
"""
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
return err(f"תיק {case_number} לא נמצא.")
pdf_uuid: UUID | None = None
if pdf_document_id:
try:
pdf_uuid = UUID(pdf_document_id)
except ValueError:
return json.dumps({"error": "pdf_document_id לא תקין"}, ensure_ascii=False)
return err("pdf_document_id לא תקין")
# INV-TOOL3 / GAP-52: idempotent on (case_id, section_id, citation, quote).
# Re-attaching the same quote to the same section returns the existing row.
@@ -49,7 +48,7 @@ async def precedent_attach(
if (_p.get("citation") == citation and _p.get("quote") == quote
and (_p.get("section_id") or None) == (section_id or None)):
_p["idempotent_existing"] = True
return json.dumps(_p, ensure_ascii=False, indent=2, default=str)
return ok(_p)
row = await db.create_case_precedent(
case_id=UUID(case["id"]),
@@ -60,17 +59,17 @@ async def precedent_attach(
pdf_document_id=pdf_uuid,
practice_area=case.get("practice_area"),
)
return json.dumps(row, ensure_ascii=False, indent=2, default=str)
return ok(row)
async def precedent_list(case_number: str) -> str:
"""רשימת כל הפסיקות שצורפו לתיק, ממוינות לפי סעיף ואז לפי זמן יצירה."""
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
return err(f"תיק {case_number} לא נמצא.")
rows = await db.list_case_precedents(UUID(case["id"]))
return json.dumps(rows, ensure_ascii=False, indent=2, default=str)
return ok(rows)
async def precedent_remove(precedent_id: str) -> str:
@@ -78,12 +77,10 @@ async def precedent_remove(precedent_id: str) -> str:
try:
pid = UUID(precedent_id)
except ValueError:
return json.dumps({"error": "precedent_id לא תקין"}, ensure_ascii=False)
return err("precedent_id לא תקין")
ok = await db.delete_case_precedent(pid)
return json.dumps(
{"deleted": ok, "precedent_id": precedent_id}, ensure_ascii=False,
)
deleted = await db.delete_case_precedent(pid)
return ok({"deleted": deleted, "precedent_id": precedent_id})
async def precedent_search_library(
@@ -97,7 +94,7 @@ async def precedent_search_library(
limit: מספר תוצאות מקסימלי
"""
if not query or len(query.strip()) < 2:
return json.dumps([], ensure_ascii=False)
return empty("שאילתה קצרה מדי (פחות מ-2 תווים).")
rows = await db.search_precedent_library(query.strip(), practice_area, limit)
return json.dumps(rows, ensure_ascii=False, indent=2, default=str)
return ok(rows)