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

@@ -1896,7 +1896,9 @@ async def api_case_create(req: CaseCreateRequest):
appeal_subtype=req.appeal_subtype,
proceeding_type=req.proceeding_type,
)
parsed = json.loads(result)
# GAP-48: case_create now returns the {status,data,message} envelope; unwrap
# to the case object so the existing gitea/appeal_subtype/paperclip wiring works.
parsed = envelope_unwrap(json.loads(result))
# Auto-create Paperclip project for the new case. case_create may have
# auto-derived appeal_subtype from the case-number prefix; prefer the
@@ -1998,10 +2000,10 @@ async def api_case_git_status(case_number: str):
async def api_case_get(case_number: str):
"""Get full case details including documents."""
result = await cases_tools.case_get(case_number)
try:
return json.loads(result)
except json.JSONDecodeError:
raise HTTPException(404, result)
parsed = json.loads(result)
if isinstance(parsed, dict) and parsed.get("status") == "error": # GAP-48
raise HTTPException(404, parsed.get("message") or result)
return envelope_unwrap(parsed)
@app.put("/api/cases/{case_number}")
@@ -2030,10 +2032,10 @@ async def api_case_update(case_number: str, req: CaseUpdateRequest, background_t
)
except ValueError as exc:
raise HTTPException(422, str(exc))
try:
parsed = json.loads(result)
except json.JSONDecodeError:
raise HTTPException(404, result)
parsed = json.loads(result)
if isinstance(parsed, dict) and parsed.get("status") == "error": # GAP-48
raise HTTPException(404, parsed.get("message") or result)
parsed = envelope_unwrap(parsed)
# Paperclip sync: update project name when title changes (fire-and-forget).
old_title = (existing or {}).get("title", "")
@@ -2075,9 +2077,9 @@ async def api_case_delete(case_number: str, remove_files: bool = False):
FK ON DELETE CASCADE; audit_log rows nullify their case_id.
Pass `remove_files=true` to also rm -rf the on-disk case directory."""
result = await cases_tools.case_delete(case_number, remove_files)
data = json.loads(result)
data = envelope_unwrap(json.loads(result)) # GAP-48
if not data.get("deleted"):
raise HTTPException(404, data.get("reason", f"תיק {case_number} לא נמצא"))
raise HTTPException(404, data.get("message") or data.get("reason") or f"תיק {case_number} לא נמצא")
return data
@@ -2085,10 +2087,10 @@ async def api_case_delete(case_number: str, remove_files: bool = False):
async def api_case_status(case_number: str):
"""Get full workflow status for a case."""
result = await workflow_tools.workflow_status(case_number)
try:
return json.loads(result)
except json.JSONDecodeError:
raise HTTPException(404, result)
parsed = json.loads(result)
if isinstance(parsed, dict) and parsed.get("status") == "error": # GAP-48
raise HTTPException(404, parsed.get("message") or result)
return envelope_unwrap(parsed)
@app.get("/api/search")
@@ -2176,7 +2178,7 @@ async def api_case_template(case_number: str):
async def api_processing_status():
"""Get overall processing status."""
result = await workflow_tools.processing_status()
return json.loads(result)
return envelope_unwrap(json.loads(result)) # GAP-48
@app.get("/api/system/diagnostics")
@@ -2965,10 +2967,10 @@ async def api_precedent_attach(case_number: str, req: PrecedentCreateRequest):
chair_note=req.chair_note,
pdf_document_id=req.pdf_document_id,
)
data = json.loads(result)
if data.get("error"):
raise HTTPException(404, data["error"])
return data
parsed = json.loads(result) # GAP-48
if parsed.get("status") == "error":
raise HTTPException(404, parsed.get("message") or "")
return envelope_unwrap(parsed)
@app.post("/api/cases/{case_number}/precedents/upload-pdf")
@@ -3022,10 +3024,10 @@ async def api_precedent_upload_pdf(
async def api_precedent_list(case_number: str):
"""List all precedents attached to a case, grouped client-side by section_id."""
result = await precedents_tools.precedent_list(case_number)
data = json.loads(result)
if isinstance(data, dict) and data.get("error"):
raise HTTPException(404, data["error"])
return data
parsed = json.loads(result) # GAP-48
if isinstance(parsed, dict) and parsed.get("status") == "error":
raise HTTPException(404, parsed.get("message") or "")
return envelope_unwrap(parsed)
@app.delete("/api/precedents/{precedent_id}")
@@ -3034,9 +3036,10 @@ async def api_precedent_delete(precedent_id: str):
in the documents table — orphaned references nullify via FK
ON DELETE SET NULL — so we keep the audit trail of the file."""
result = await precedents_tools.precedent_remove(precedent_id)
data = json.loads(result)
if data.get("error"):
raise HTTPException(400, data["error"])
parsed = json.loads(result) # GAP-48
if parsed.get("status") == "error":
raise HTTPException(400, parsed.get("message") or "")
data = envelope_unwrap(parsed)
if not data.get("deleted"):
raise HTTPException(404, "לא נמצא")
return data
@@ -3046,7 +3049,10 @@ async def api_precedent_delete(precedent_id: str):
async def api_precedent_search(q: str, practice_area: str = "", limit: int = 10):
"""Cross-case library typeahead. Returns one row per distinct citation."""
result = await precedents_tools.precedent_search_library(q, practice_area, limit)
return json.loads(result)
parsed = json.loads(result) # GAP-48: typeahead expects an array
if isinstance(parsed, dict) and parsed.get("status") == "error":
raise HTTPException(400, parsed.get("message") or "")
return parsed.get("data") or []
# ── Exports API — drafts, versions, download, upload, mark-final ──
@@ -3443,9 +3449,10 @@ async def api_start_workflow(case_number: str):
"""
# 1. Verify case exists and status is appropriate
case_raw = await cases_tools.case_get(case_number)
case_data = json.loads(case_raw)
if "error" in case_data:
case_env = json.loads(case_raw) # GAP-48
if isinstance(case_env, dict) and case_env.get("status") == "error":
raise HTTPException(404, f"תיק {case_number} לא נמצא")
case_data = envelope_unwrap(case_env)
status = case_data.get("status", "")
allowed = {"new", "documents_ready"}