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

@@ -14,6 +14,7 @@ import httpx
from legal_mcp import config
from legal_mcp.services import audit, db, extractor, git_sync, practice_area as pa
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
logger = logging.getLogger(__name__)
@@ -158,7 +159,7 @@ async def case_create(
_existing = await db.get_case_by_number(case_number)
if _existing:
_existing["idempotent_existing"] = True
return json.dumps(_existing, default=str, ensure_ascii=False, indent=2)
return ok(_existing)
from datetime import date as date_type
@@ -257,7 +258,7 @@ async def case_create(
# silently producing a case with no remote.
case["gitea"] = await _setup_gitea_remote(case_number, title, case_dir)
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
return ok(case)
async def case_list(status: str = "", limit: int = 50) -> str:
@@ -272,8 +273,8 @@ async def case_list(status: str = "", limit: int = 50) -> str:
"""
cases = await db.list_cases(status=status or None, limit=limit)
if not cases:
return "אין תיקים."
return json.dumps(cases, default=str, ensure_ascii=False, indent=2)
return empty("אין תיקים.")
return ok(cases)
async def case_get(case_number: str) -> str:
@@ -284,11 +285,11 @@ async def case_get(case_number: str) -> str:
"""
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
return err(f"תיק {case_number} לא נמצא.")
docs = await db.list_documents(UUID(case["id"]))
case["documents"] = docs
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
return ok(case)
async def case_update(
@@ -338,7 +339,7 @@ async def case_update(
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
return err(f"תיק {case_number} לא נמצא.")
fields = {}
if status:
@@ -395,7 +396,7 @@ async def case_update(
except Exception:
pass # git not available — non-critical
return json.dumps(updated, default=str, ensure_ascii=False, indent=2)
return ok(updated)
async def case_delete(case_number: str, remove_files: bool = False) -> str:
@@ -408,28 +409,25 @@ async def case_delete(case_number: str, remove_files: bool = False) -> str:
"""
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps(
{"deleted": False, "reason": f"תיק {case_number} לא נמצא."},
ensure_ascii=False,
)
return err(f"תיק {case_number} לא נמצא.")
case_id = UUID(case["id"])
ok = await db.delete_case(case_id)
deleted = await db.delete_case(case_id)
result = {
"deleted": ok,
"deleted": deleted,
"case_number": case_number,
"case_id": str(case_id),
"removed_files": False,
}
if ok and remove_files:
if deleted and remove_files:
case_dir = config.find_case_dir(case_number)
if case_dir.exists():
shutil.rmtree(case_dir, ignore_errors=True)
result["removed_files"] = True
return json.dumps(result, ensure_ascii=False, indent=2)
return ok(result)
async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
@@ -456,27 +454,24 @@ async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
break
if final_path is None:
return json.dumps({
"status": "not_found",
"case_number": case_number,
"expected_path": str(exports_dir / f"{final_stem}.docx"),
"tried_extensions": [".docx", ".pdf", ".doc", ".rtf", ".txt", ".md"],
"hint": (
"ההחלטה הסופית עדיין לא סומנה כ'סופית' ב-UI. "
"דפנה צריכה ללחוץ 'סמן כסופי' על קובץ הטיוטה הנכון."
),
}, ensure_ascii=False, indent=2)
return err(
"ההחלטה הסופית עדיין לא סומנה כ'סופית' ב-UI. "
"דפנה צריכה ללחוץ 'סמן כסופי' על קובץ הטיוטה הנכון.",
data={
"case_number": case_number,
"expected_path": str(exports_dir / f"{final_stem}.docx"),
"tried_extensions": [".docx", ".pdf", ".doc", ".rtf", ".txt", ".md"],
},
)
try:
text, page_count, _ = await extractor.extract_text(str(final_path))
except Exception as e:
logger.exception("case_get_final_text: extraction failed for %s", case_number)
return json.dumps({
"status": "error",
"case_number": case_number,
"file_path": str(final_path),
"error": str(e),
}, ensure_ascii=False, indent=2)
return err(
f"חילוץ הטקסט נכשל: {e}",
data={"case_number": case_number, "file_path": str(final_path)},
)
text = text or ""
truncated = False
@@ -484,12 +479,11 @@ async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
text = text[:max_chars]
truncated = True
return json.dumps({
"status": "ok",
return ok({
"case_number": case_number,
"file_path": str(final_path),
"text_length": len(text),
"page_count": page_count,
"truncated": truncated,
"text": text,
}, ensure_ascii=False, indent=2)
})