feat(mcp): FU-14 GAP-48 פרוסה 1 — envelope אחיד (SSoT) + משפחת-חיפוש

INV-TOOL1: כלי-ה-MCP החזירו 3 מוסכמות סותרות (raw payload / {error} /
{status,message} אד-הוק) + 5 עותקי _ok/_err משוכפלים. נוצר tools/envelope.py
כמקור-אמת יחיד: ok/empty/err → {status,data,message}, כש-status מבחין
מפורשות הצלחה/ריק/שגיאה.

פרוסה 1 ממירה את משפחת-החיפוש (search_decisions, search_case_documents,
find_similar_cases, search_internal_decisions). web/app.py מפרק את המעטפת
דרך envelope_unwrap כדי לשמר את חוזה-ה-UI↔API (X6) ללא-שינוי — תשובת ה-HTTP
זהה (list על hits, {"message"} על ריק/שגיאה). טסט test_search_domain_scope
עודכן לחוזה החדש (5/5 עוברים).

החלטה: הדרגתי לפי-משפחה ולא big-bang. מפת-צרכנים: server.py pass-through,
web-ui מבודד (/api/*), רק 17 כלים נצרכים ישירות מ-app.py → סיכון מינימלי
לסוכנים החיים. ~73 כלים נותרו לפרוסות הבאות.

Invariants: מקדם INV-TOOL1 (envelope עקבי) + G2 (SSoT, ביטול כפילות _ok/_err).
לא נוגע ב-G1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:32:07 +00:00
parent c52b5986a3
commit aa0a736a7b
6 changed files with 92 additions and 26 deletions

View File

@@ -84,7 +84,8 @@ def test_explicit_practice_area_used(patched: dict) -> None:
# explicit value must not trigger a case lookup
assert patched["cases"] == {}
# ran -> JSON result, not an error string
assert json.loads(out)[0]["content"] == "hit"
assert json.loads(out)["status"] == "ok"
assert json.loads(out)["data"][0]["content"] == "hit"
def test_case_practice_area_used(patched: dict) -> None:
@@ -98,7 +99,8 @@ def test_case_practice_area_used(patched: dict) -> None:
)
assert len(patched["hybrid"]) == 1
assert patched["hybrid"][0]["practice_area"] == "betterment_levy"
assert json.loads(out)[0]["content"] == "hit"
assert json.loads(out)["status"] == "ok"
assert json.loads(out)["data"][0]["content"] == "hit"
def test_case_empty_practice_area_derived_from_prefix(patched: dict) -> None:
@@ -113,7 +115,8 @@ def test_case_empty_practice_area_derived_from_prefix(patched: dict) -> None:
)
assert len(patched["hybrid"]) == 1
assert patched["hybrid"][0]["practice_area"] == "betterment_levy"
assert json.loads(out)[0]["content"] == "hit"
assert json.loads(out)["status"] == "ok"
assert json.loads(out)["data"][0]["content"] == "hit"
def test_case_undeterminable_is_blocked(patched: dict) -> None:
@@ -128,11 +131,10 @@ def test_case_undeterminable_is_blocked(patched: dict) -> None:
)
# hybrid search must NOT have been called
assert patched["hybrid"] == []
# returns a Hebrew error string, not JSON
assert out.startswith("שגיאה")
assert "7777/25" in out
with pytest.raises(json.JSONDecodeError):
json.loads(out)
# GAP-48: returns the {status,data,message} envelope with status="error"
parsed = json.loads(out)
assert parsed["status"] == "error"
assert "7777/25" in parsed["message"]
def test_no_case_no_practice_area_proceeds(patched: dict) -> None:
@@ -141,4 +143,5 @@ def test_no_case_no_practice_area_proceeds(patched: dict) -> None:
assert len(patched["hybrid"]) == 1
assert patched["hybrid"][0]["practice_area"] is None
assert patched["cases"] == {}
assert json.loads(out)[0]["content"] == "hit"
assert json.loads(out)["status"] == "ok"
assert json.loads(out)["data"][0]["content"] == "hit"