feat(mcp): FU-14 GAP-48 פרוסה 3 — envelope למשפחת drafting (סגירת GAP-48)
הפרוסה האחרונה של GAP-48 (INV-TOOL1). 18 כלי drafting הומרו ל-{status,data,message}
דרך tools/envelope.py — כולל מסלול הפקת-ההחלטה הקריטי.
עיקרון לכלים עם כשל משמעותי (export_docx/revise_draft/apply_user_edit): err()
ברמת-המעטפת — כך שהסוכן והמשתמש רואים את הכשל; failed_gates רוכב ב-data.
שאר הכלים: ok(data=payload) להצלחה, err להיעדר-תיק/קלט-שגוי/חריגה.
6 צרכני-app.py חוּוטו (get_decision_template, apply_user_edit ×2, revise_draft,
list_bookmarks, export_docx) עם envelope_unwrap + בדיקת status=="error"→4xx,
לשמירת חוזה-ה-API (X6) ללא-שינוי. test_export_qa_gate עודכן לחוזה החדש.
בדיקות: 182/182 עוברים (כולל שערי-QA של הייצוא).
GAP-48 סגור: כל ~12 משפחות-הכלים אחידות. נותר ב-FU-14: GAP-49/50 (שובר), GAP-54.
Invariants: משלים INV-TOOL1 + G2. מתועד ב-X9 (נסגר) + gap-audit פרוסה 7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
web/app.py
70
web/app.py
@@ -2169,9 +2169,10 @@ async def api_search_cases(q: str, limit: int = 10):
|
||||
async def api_case_template(case_number: str):
|
||||
"""Get outcome-aware decision template for a case."""
|
||||
result = await drafting_tools.get_decision_template(case_number)
|
||||
if result.startswith("תיק"):
|
||||
raise HTTPException(404, result)
|
||||
return {"template": result}
|
||||
parsed = json.loads(result) # GAP-48
|
||||
if parsed.get("status") == "error":
|
||||
raise HTTPException(404, parsed.get("message") or "")
|
||||
return {"template": envelope_unwrap(parsed)}
|
||||
|
||||
|
||||
@app.get("/api/processing-status")
|
||||
@@ -3145,7 +3146,11 @@ async def api_upload_export(case_number: str, file: UploadFile = File(...)):
|
||||
auto_result: dict = {"status": "ok"}
|
||||
try:
|
||||
raw = await drafting_tools.apply_user_edit(case_number, dest.name)
|
||||
auto_result = json.loads(raw)
|
||||
parsed = json.loads(raw) # GAP-48
|
||||
if parsed.get("status") == "error":
|
||||
auto_result = {"status": "error", "message": parsed.get("message", "")}
|
||||
else:
|
||||
auto_result = {**(envelope_unwrap(parsed) or {}), "status": "completed"}
|
||||
except Exception as e:
|
||||
auto_result = {"status": "error", "message": str(e)}
|
||||
|
||||
@@ -3174,37 +3179,29 @@ async def api_revise_draft(case_number: str, req: ReviseRequest):
|
||||
json.dumps(req.revisions, ensure_ascii=False),
|
||||
req.author,
|
||||
)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(500, raw)
|
||||
if data.get("status") == "error":
|
||||
raise HTTPException(400, data.get("message", "revise failed"))
|
||||
return data
|
||||
parsed = json.loads(raw) # GAP-48
|
||||
if parsed.get("status") == "error":
|
||||
raise HTTPException(400, parsed.get("message", "revise failed"))
|
||||
return envelope_unwrap(parsed)
|
||||
|
||||
|
||||
@app.get("/api/cases/{case_number}/exports/bookmarks")
|
||||
async def api_list_bookmarks(case_number: str):
|
||||
"""List bookmarks in the case's active draft (anchors for revisions)."""
|
||||
raw = await drafting_tools.list_bookmarks(case_number)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(500, raw)
|
||||
return data
|
||||
parsed = json.loads(raw) # GAP-48
|
||||
data = envelope_unwrap(parsed)
|
||||
return data if isinstance(data, dict) else {"bookmarks": []}
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/exports/{filename}/retrofit")
|
||||
async def api_retrofit_bookmarks(case_number: str, filename: str):
|
||||
"""Manually trigger retrofit of bookmarks on an existing file."""
|
||||
raw = await drafting_tools.apply_user_edit(case_number, filename)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(500, raw)
|
||||
if data.get("status") == "error":
|
||||
raise HTTPException(400, data.get("message", "retrofit failed"))
|
||||
return data
|
||||
parsed = json.loads(raw) # GAP-48
|
||||
if parsed.get("status") == "error":
|
||||
raise HTTPException(400, parsed.get("message", "retrofit failed"))
|
||||
return envelope_unwrap(parsed)
|
||||
|
||||
|
||||
@app.get("/api/cases/{case_number}/active-draft")
|
||||
@@ -3323,26 +3320,21 @@ async def api_export_docx(case_number: str, background_tasks: BackgroundTasks):
|
||||
(markdown body + download link) to the linked issue.
|
||||
"""
|
||||
result = await drafting_tools.export_docx(case_number)
|
||||
if isinstance(result, dict):
|
||||
data = result
|
||||
else:
|
||||
try:
|
||||
data = json.loads(result)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# export_docx can also return a plain (non-JSON) string, e.g.
|
||||
# "תיק ... לא נמצא." — surface it as a 500 with the raw text.
|
||||
raise HTTPException(500, str(result))
|
||||
parsed = json.loads(result) # GAP-48: tool returns the {status,data,message} envelope
|
||||
|
||||
# FU-6: a QA gate (or another error) can block the export. export_docx
|
||||
# signals this with status == "error". Returning the existing 200 here
|
||||
# would let the UI show a false "exported successfully" toast, so we map
|
||||
# a block to 409 Conflict carrying the Hebrew message + failed_gates.
|
||||
if isinstance(data, dict) and data.get("status") == "error":
|
||||
detail = {"message": data.get("message", "ייצוא נחסם.")}
|
||||
if data.get("failed_gates"):
|
||||
detail["failed_gates"] = data["failed_gates"]
|
||||
# signals this with envelope status == "error". Returning the existing 200
|
||||
# here would let the UI show a false "exported successfully" toast, so we map
|
||||
# a block to 409 Conflict carrying the Hebrew message + failed_gates (in data).
|
||||
if parsed.get("status") == "error":
|
||||
detail = {"message": parsed.get("message", "ייצוא נחסם.")}
|
||||
inner = parsed.get("data") or {}
|
||||
if inner.get("failed_gates"):
|
||||
detail["failed_gates"] = inner["failed_gates"]
|
||||
raise HTTPException(409, detail)
|
||||
|
||||
data = envelope_unwrap(parsed) # success payload: {path, active_draft_path, message}
|
||||
|
||||
# Notify the Paperclip plugin to attach the final-decision document.
|
||||
docx_filename = (
|
||||
data.get("filename")
|
||||
|
||||
Reference in New Issue
Block a user