Merge pull request 'feat(mcp): FU-14 GAP-48 פרוסה 3 — envelope ל-drafting (סגירת GAP-48)' (#79) from fix/fu14-gap48-drafting into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m42s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m42s
This commit was merged in pull request #79.
This commit is contained in:
@@ -38,8 +38,8 @@
|
|||||||
מקביל ל-[X6 INV-UI3](X6-ui-api-contract.md). **הנדסי.**
|
מקביל ל-[X6 INV-UI3](X6-ui-api-contract.md). **הנדסי.**
|
||||||
**מקורות:** Anthropic — *MCP / tool result conventions* (https://modelcontextprotocol.io/) ·
|
**מקורות:** Anthropic — *MCP / tool result conventions* (https://modelcontextprotocol.io/) ·
|
||||||
JSON-RPC 2.0 (result/error envelope) (https://www.jsonrpc.org/specification) · RFC 9457 (Problem Details) | סטטוס: verified
|
JSON-RPC 2.0 (result/error envelope) (https://www.jsonrpc.org/specification) · RFC 9457 (Problem Details) | סטטוס: verified
|
||||||
**אכיפה:** wrapper-תשובה משותף בכל הכלים — `tools/envelope.py` (`ok`/`empty`/`err` → `{status,data,message}`, status ∈ ok/empty/error — מבחין הצלחה/ריק/שגיאה), SSoT יחיד שמחליף את 5 ה-`_ok`/`_err` המשוכפלים. עיקרון: envelope-`status` משקף אם **הקריאה לכלי** הצליחה; תוצאות-עסקיות (completed/failed_gates/...) נשמרות בתוך `data`. צרכני-API ב-`web/app.py` מפרקים דרך `envelope_unwrap` כדי לשמר את חוזה-ה-UI↔API (X6) ללא-שינוי. **GAP-48 בתהליך (2026-06-06):** הומרו — search · precedent_library · citations · internal_decisions · missing_precedents · training_enrichment · precedents · legal_arguments · cases · documents · workflow (~11 משפחות, ~59 כלים). **נותר:** משפחת `drafting` (18 כלים — מסלול הפקת-ההחלטה הקריטי) בפרוסה נפרדת עם שער-טסט-ייצוא.
|
**אכיפה:** wrapper-תשובה משותף בכל הכלים — `tools/envelope.py` (`ok`/`empty`/`err` → `{status,data,message}`, status ∈ ok/empty/error — מבחין הצלחה/ריק/שגיאה), SSoT יחיד שמחליף את 5 ה-`_ok`/`_err` המשוכפלים. עיקרון: envelope-`status` משקף אם **הקריאה לכלי** הצליחה; תוצאות-עסקיות (failed_gates/results/...) נשמרות בתוך `data`. צרכני-API ב-`web/app.py` מפרקים דרך `envelope_unwrap` (+בדיקת `status=="error"`→4xx) כדי לשמר את חוזה-ה-UI↔API (X6) ללא-שינוי. **GAP-48 ✅ הושלם (2026-06-06):** כל ~12 משפחות-הכלים הומרו ל-envelope (search · precedent_library · citations · internal_decisions · missing_precedents · training_enrichment · precedents · legal_arguments · cases · documents · workflow · drafting). מסלול הפקת-ההחלטה (`export_docx` שער-QA) מאומת ב-`test_export_qa_gate`. 182/182 טסטים עוברים.
|
||||||
**הפרה ידועה:** משפחת drafting עדיין מעורבת ({status,message} אד-הוק / מחרוזות) — תיושר בפרוסת drafting.
|
**הפרה ידועה:** — (נסגר)
|
||||||
|
|
||||||
### INV-TOOL2: שמות עקביים + חיפוש לפי-קורפוס
|
### INV-TOOL2: שמות עקביים + חיפוש לפי-קורפוס
|
||||||
**כלל:** שמות-הכלים עוקבים אחר convention אחיד, ושם משקף התנהגות. כלי-חיפוש מובחנים **לפי הקורפוס**
|
**כלל:** שמות-הכלים עוקבים אחר convention אחיד, ושם משקף התנהגות. כלי-חיפוש מובחנים **לפי הקורפוס**
|
||||||
|
|||||||
@@ -203,7 +203,8 @@
|
|||||||
- **סטטוס חלקי (פרוסה 3, 2026-06-06):** ✅ **GAP-51** (set_outcome SSoT, הכרעת-יו"ר "3 תוצאות + הוצאת betterment_levy") — קנוני `rejection/partial_acceptance/full_acceptance` ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` (עברית-ב-UI SSoT); `canonical_outcome()` ל-legacy; `betterment_levy`→`PRACTICE_AREA_OVERRIDES` (override לפי practice_area); block_writer/set_outcome/drafting/web-ui יושרו; נתונים נורמלו (9 שורות + גיבוי).
|
- **סטטוס חלקי (פרוסה 3, 2026-06-06):** ✅ **GAP-51** (set_outcome SSoT, הכרעת-יו"ר "3 תוצאות + הוצאת betterment_levy") — קנוני `rejection/partial_acceptance/full_acceptance` ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` (עברית-ב-UI SSoT); `canonical_outcome()` ל-legacy; `betterment_levy`→`PRACTICE_AREA_OVERRIDES` (override לפי practice_area); block_writer/set_outcome/drafting/web-ui יושרו; נתונים נורמלו (9 שורות + גיבוי).
|
||||||
- **סטטוס חלקי (פרוסה 4, 2026-06-06):** ✅ **GAP-47 (חלק provenance, INV-TOOL4/G9)** — `draft_section` חושף בפלט `document_id`+`page`+`score` לכל קטע ב-`case_documents`/`precedents` (ה-provenance כבר נשלף ב-`search_similar` ונזרק; כעת מוחזר), כך שהכותב יכול לעקוב אל המקור ולצטטו. תוספתי, לא-שובר. **נותר ב-GAP-47:** העברת הנחיות-יו"ר מ-`analysis-and-research.md` ל-DB (`get_chair_directions`) — שינוי-מסלול גדול יותר הנוגע ל-UI של דפנה ולזרימת-האנליסט; לפרוסה נפרדת.
|
- **סטטוס חלקי (פרוסה 4, 2026-06-06):** ✅ **GAP-47 (חלק provenance, INV-TOOL4/G9)** — `draft_section` חושף בפלט `document_id`+`page`+`score` לכל קטע ב-`case_documents`/`precedents` (ה-provenance כבר נשלף ב-`search_similar` ונזרק; כעת מוחזר), כך שהכותב יכול לעקוב אל המקור ולצטטו. תוספתי, לא-שובר. **נותר ב-GAP-47:** העברת הנחיות-יו"ר מ-`analysis-and-research.md` ל-DB (`get_chair_directions`) — שינוי-מסלול גדול יותר הנוגע ל-UI של דפנה ולזרימת-האנליסט; לפרוסה נפרדת.
|
||||||
- **סטטוס חלקי (פרוסה 5, 2026-06-06):** 🔄 **GAP-48 (envelope אחיד, INV-TOOL1) — תחילת מיגרציה הדרגתית.** נוצר `tools/envelope.py` כ-SSoT יחיד (`ok`/`empty`/`err` → `{status,data,message}`, status מבחין הצלחה/ריק/שגיאה) המחליף 3 מוסכמות סותרות (raw payload / `{error}` / `{status,message}` אד-הוק) ו-5 עותקי `_ok`/`_err` משוכפלים. **משפחת-החיפוש הראשונה הומרה** (`search_decisions`/`search_case_documents`/`find_similar_cases`/`search_internal_decisions`); `web/app.py` מפרק דרך `envelope_unwrap` לשמירת חוזה-ה-API (X6) ללא-שינוי; טסט `test_search_domain_scope` עודכן לחוזה החדש (5/5 ✅). **החלטה הנדסית:** הדרגתי לפי-משפחה ולא big-bang — מפת-צרכנים (Explore) הראתה ש-server.py הוא pass-through, web-ui מבודד (`/api/*`), ורק 17 כלים נצרכים ישירות מ-app.py — כך הסיכון לסוכנים החיים ממוזער. נותרו ~73 כלים בפרוסות הבאות.
|
- **סטטוס חלקי (פרוסה 5, 2026-06-06):** 🔄 **GAP-48 (envelope אחיד, INV-TOOL1) — תחילת מיגרציה הדרגתית.** נוצר `tools/envelope.py` כ-SSoT יחיד (`ok`/`empty`/`err` → `{status,data,message}`, status מבחין הצלחה/ריק/שגיאה) המחליף 3 מוסכמות סותרות (raw payload / `{error}` / `{status,message}` אד-הוק) ו-5 עותקי `_ok`/`_err` משוכפלים. **משפחת-החיפוש הראשונה הומרה** (`search_decisions`/`search_case_documents`/`find_similar_cases`/`search_internal_decisions`); `web/app.py` מפרק דרך `envelope_unwrap` לשמירת חוזה-ה-API (X6) ללא-שינוי; טסט `test_search_domain_scope` עודכן לחוזה החדש (5/5 ✅). **החלטה הנדסית:** הדרגתי לפי-משפחה ולא big-bang — מפת-צרכנים (Explore) הראתה ש-server.py הוא pass-through, web-ui מבודד (`/api/*`), ורק 17 כלים נצרכים ישירות מ-app.py — כך הסיכון לסוכנים החיים ממוזער. נותרו ~73 כלים בפרוסות הבאות.
|
||||||
- **סטטוס חלקי (פרוסה 6, 2026-06-06):** 🔄 **GAP-48 — מיגרציה רוחבית.** הומרו 10 משפחות נוספות ל-envelope: `precedent_library` (14), `citations` (3), `internal_decisions` (1), `missing_precedents` (4), `training_enrichment` (2), `precedents` (4), `legal_arguments` (2), `cases` (7), `documents` (8), `workflow` (9). בוטלו 5 עותקי `_ok`/`_err` משוכפלים (alias ל-SSoT, G2). עיקרון: envelope-`status` = הצלחת-הקריאה; תוצאה-עסקית (idempotent_existing/noop/...) ב-`data`. צרכני-app.py של cases/workflow/precedents חוּוטו דרך `envelope_unwrap` + בדיקת `status=="error"`→4xx, לשמירת חוזה-ה-API. כל הטסטים עוברים (182/182; `test_corpus_constraints` עודכן לחוזה). **נותר:** משפחת `drafting` (18 כלים — מסלול הפקת-ההחלטה) בפרוסה נפרדת. נותר ב-FU-14: GAP-48 (drafting), GAP-49/50 (מיזוג+rename — שובר), GAP-54 (איחוד קליטת-פסיקה).
|
- **סטטוס חלקי (פרוסה 6, 2026-06-06):** 🔄 **GAP-48 — מיגרציה רוחבית.** הומרו 10 משפחות נוספות ל-envelope: `precedent_library` (14), `citations` (3), `internal_decisions` (1), `missing_precedents` (4), `training_enrichment` (2), `precedents` (4), `legal_arguments` (2), `cases` (7), `documents` (8), `workflow` (9). בוטלו 5 עותקי `_ok`/`_err` משוכפלים (alias ל-SSoT, G2). עיקרון: envelope-`status` = הצלחת-הקריאה; תוצאה-עסקית (idempotent_existing/noop/...) ב-`data`. צרכני-app.py של cases/workflow/precedents חוּוטו דרך `envelope_unwrap` + בדיקת `status=="error"`→4xx, לשמירת חוזה-ה-API. כל הטסטים עוברים (182/182; `test_corpus_constraints` עודכן לחוזה). **נותר:** משפחת `drafting` (18 כלים — מסלול הפקת-ההחלטה) בפרוסה נפרדת.
|
||||||
|
- **פרוסה 7, 2026-06-06 — ✅ GAP-48 הושלם.** משפחת `drafting` (18 כלים) הומרה ל-envelope. export_docx/revise_draft/apply_user_edit משתמשים ב-`err`-לכשל (כך שהסוכן והמשתמש רואים את הכשל ברמת-המעטפת), כש-`failed_gates` רוכב ב-`data`; 6 צרכני-app.py (get_decision_template/apply_user_edit×2/revise_draft/list_bookmarks/export_docx) חוּוטו עם בדיקת envelope-status; `test_export_qa_gate` עודכן לחוזה (182/182 עוברים). **GAP-48 סגור — כל ~12 המשפחות אחידות.** נותר ב-FU-14: GAP-49/50 (מיזוג+rename — שובר), GAP-54 (איחוד קליטת-פסיקה), GAP-47-חלק-ב (הנחיות-יו"ר→DB).
|
||||||
|
|
||||||
### FU-15 — deploy/env/secrets
|
### FU-15 — deploy/env/secrets
|
||||||
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from legal_mcp.services.lessons import (
|
|||||||
format_ratios_comment,
|
format_ratios_comment,
|
||||||
get_lessons_for_outcome,
|
get_lessons_for_outcome,
|
||||||
)
|
)
|
||||||
|
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||||
|
|
||||||
# Fallback template for cases without expected_outcome
|
# Fallback template for cases without expected_outcome
|
||||||
DECISION_TEMPLATE = """# החלטה
|
DECISION_TEMPLATE = """# החלטה
|
||||||
@@ -185,7 +186,7 @@ async def get_style_guide() -> str:
|
|||||||
result += f"- **פתיחה:** {_op['description']} ({_op['paragraphs'][0]}-{_op['paragraphs'][1]} פסקאות)\n"
|
result += f"- **פתיחה:** {_op['description']} ({_op['paragraphs'][0]}-{_op['paragraphs'][1]} פסקאות)\n"
|
||||||
result += f"- **סיכום ({_sm['heading']}):** {_sm['description']}\n\n"
|
result += f"- **סיכום ({_sm['heading']}):** {_sm['description']}\n\n"
|
||||||
|
|
||||||
return result
|
return ok(result)
|
||||||
|
|
||||||
|
|
||||||
async def draft_section(
|
async def draft_section(
|
||||||
@@ -206,7 +207,7 @@ async def draft_section(
|
|||||||
"""
|
"""
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
expected_outcome = case.get("expected_outcome", "")
|
expected_outcome = case.get("expected_outcome", "")
|
||||||
@@ -309,7 +310,7 @@ async def draft_section(
|
|||||||
|
|
||||||
context["drafting_guidance"] = guidance
|
context["drafting_guidance"] = guidance
|
||||||
|
|
||||||
return json.dumps(context, ensure_ascii=False, indent=2)
|
return ok(context)
|
||||||
|
|
||||||
|
|
||||||
async def get_chair_directions(case_number: str) -> str:
|
async def get_chair_directions(case_number: str) -> str:
|
||||||
@@ -335,7 +336,7 @@ async def get_chair_directions(case_number: str) -> str:
|
|||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
file_path = case_dir / "documents" / "research" / "analysis-and-research.md"
|
file_path = case_dir / "documents" / "research" / "analysis-and-research.md"
|
||||||
result = research_md.extract_chair_directions(file_path)
|
result = research_md.extract_chair_directions(file_path)
|
||||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
|
|
||||||
|
|
||||||
async def get_decision_template(case_number: str) -> str:
|
async def get_decision_template(case_number: str) -> str:
|
||||||
@@ -348,7 +349,7 @@ async def get_decision_template(case_number: str) -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
# GAP-51: canonicalize outcome + apply betterment_levy practice_area override.
|
# GAP-51: canonicalize outcome + apply betterment_levy practice_area override.
|
||||||
expected_outcome = canonical_outcome(case.get("expected_outcome", ""))
|
expected_outcome = canonical_outcome(case.get("expected_outcome", ""))
|
||||||
@@ -386,7 +387,7 @@ async def get_decision_template(case_number: str) -> str:
|
|||||||
f"<!-- פתיחת דיון: {opening['description']} -->\n"
|
f"<!-- פתיחת דיון: {opening['description']} -->\n"
|
||||||
f"<!-- סיכום: {summary['description']} -->\n\n"
|
f"<!-- סיכום: {summary['description']} -->\n\n"
|
||||||
)
|
)
|
||||||
return header + template
|
return ok(header + template)
|
||||||
else:
|
else:
|
||||||
# Fallback to generic template
|
# Fallback to generic template
|
||||||
template = DECISION_TEMPLATE.format(**format_args)
|
template = DECISION_TEMPLATE.format(**format_args)
|
||||||
@@ -395,7 +396,7 @@ async def get_decision_template(case_number: str) -> str:
|
|||||||
"<!-- לא הוגדרה תוצאה צפויה. הגדר expected_outcome בתיק לקבלת תבנית מותאמת. -->\n"
|
"<!-- לא הוגדרה תוצאה צפויה. הגדר expected_outcome בתיק לקבלת תבנית מותאמת. -->\n"
|
||||||
f"<!-- ערכים אפשריים: {', '.join(VALID_OUTCOMES)} -->\n\n"
|
f"<!-- ערכים אפשריים: {', '.join(VALID_OUTCOMES)} -->\n\n"
|
||||||
) + template
|
) + template
|
||||||
return template
|
return ok(template)
|
||||||
|
|
||||||
|
|
||||||
async def validate_decision(case_number: str) -> str:
|
async def validate_decision(case_number: str) -> str:
|
||||||
@@ -408,15 +409,15 @@ async def validate_decision(case_number: str) -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await qa_validator.validate_decision(case_id)
|
result = await qa_validator.validate_decision(case_id)
|
||||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
return err(str(e))
|
||||||
|
|
||||||
|
|
||||||
async def export_docx(case_number: str, output_path: str = "") -> str:
|
async def export_docx(case_number: str, output_path: str = "") -> str:
|
||||||
@@ -433,7 +434,7 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
@@ -441,21 +442,17 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
|
|||||||
# fail (or before QA has been run at all). Gate on the STORED qa_results —
|
# fail (or before QA has been run at all). Gate on the STORED qa_results —
|
||||||
# cheap SELECT, no LLM re-run.
|
# cheap SELECT, no LLM re-run.
|
||||||
if not await db.qa_run_exists(case_id):
|
if not await db.qa_run_exists(case_id):
|
||||||
return json.dumps({
|
return err("ייצוא נחסם: בקרת איכות (QA) טרם רצה על התיק. "
|
||||||
"status": "error",
|
"הרץ validate_decision לפני ייצוא.")
|
||||||
"message": "ייצוא נחסם: בקרת איכות (QA) טרם רצה על התיק. "
|
|
||||||
"הרץ validate_decision לפני ייצוא.",
|
|
||||||
}, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
critical = await db.get_critical_qa_failures(case_id)
|
critical = await db.get_critical_qa_failures(case_id)
|
||||||
if critical:
|
if critical:
|
||||||
gate_names = ", ".join(r["check_name"] for r in critical)
|
gate_names = ", ".join(r["check_name"] for r in critical)
|
||||||
return json.dumps({
|
return err(
|
||||||
"status": "error",
|
f"ייצוא נחסם: שערי QA קריטיים נכשלו ({gate_names}). "
|
||||||
"message": f"ייצוא נחסם: שערי QA קריטיים נכשלו ({gate_names}). "
|
f"תקן את הליקויים והרץ validate_decision מחדש לפני ייצוא.",
|
||||||
f"תקן את הליקויים והרץ validate_decision מחדש לפני ייצוא.",
|
data={"failed_gates": [r["check_name"] for r in critical]},
|
||||||
"failed_gates": [r["check_name"] for r in critical],
|
)
|
||||||
}, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path = await docx_exporter.export_decision(case_id, output_path or None)
|
path = await docx_exporter.export_decision(case_id, output_path or None)
|
||||||
@@ -469,17 +466,13 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
|
|||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
if case_dir.exists():
|
if case_dir.exists():
|
||||||
git_sync.commit_and_push(case_dir, f"ייצוא DOCX: {Path(path).name}")
|
git_sync.commit_and_push(case_dir, f"ייצוא DOCX: {Path(path).name}")
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"path": path,
|
"path": path,
|
||||||
"active_draft_path": path,
|
"active_draft_path": path,
|
||||||
"message": f"DOCX נוצר: {path}",
|
"message": f"DOCX נוצר: {path}",
|
||||||
}, ensure_ascii=False, indent=2)
|
})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return json.dumps({
|
return err(str(e))
|
||||||
"status": "error",
|
|
||||||
"message": str(e),
|
|
||||||
}, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
# ── Interim draft (pre-ruling) ────────────────────────────────────
|
# ── Interim draft (pre-ruling) ────────────────────────────────────
|
||||||
@@ -503,16 +496,13 @@ async def extract_appraiser_facts(case_number: str) -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
try:
|
try:
|
||||||
result = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
|
result = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
|
||||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_appraiser_facts(case_number: str) -> str:
|
async def get_appraiser_facts(case_number: str) -> str:
|
||||||
@@ -527,23 +517,19 @@ async def get_appraiser_facts(case_number: str) -> str:
|
|||||||
"""
|
"""
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
try:
|
try:
|
||||||
facts = await db.list_appraiser_facts(case_id)
|
facts = await db.list_appraiser_facts(case_id)
|
||||||
conflicts = await db.detect_appraiser_conflicts(case_id)
|
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "ok",
|
|
||||||
"case_number": case_number,
|
"case_number": case_number,
|
||||||
"count": len(facts),
|
"count": len(facts),
|
||||||
"facts": facts,
|
"facts": facts,
|
||||||
"conflicts": conflicts,
|
"conflicts": conflicts,
|
||||||
}, default=str, ensure_ascii=False, indent=2)
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
||||||
@@ -562,9 +548,7 @@ async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
# Make sure appraiser facts exist before writing block-tet (which depends on them).
|
# Make sure appraiser facts exist before writing block-tet (which depends on them).
|
||||||
@@ -593,13 +577,12 @@ async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
|||||||
"error": str(e),
|
"error": str(e),
|
||||||
})
|
})
|
||||||
|
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"blocks": results,
|
"blocks": results,
|
||||||
"appraiser_facts_run": facts_run,
|
"appraiser_facts_run": facts_run,
|
||||||
"total_words": sum(r.get("word_count", 0) for r in results),
|
"total_words": sum(r.get("word_count", 0) for r in results),
|
||||||
"completed": sum(1 for r in results if r["status"] == "completed"),
|
"completed": sum(1 for r in results if r["status"] == "completed"),
|
||||||
}, default=str, ensure_ascii=False, indent=2)
|
})
|
||||||
|
|
||||||
|
|
||||||
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
||||||
@@ -615,9 +598,7 @@ async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -628,16 +609,14 @@ async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
|||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
if case_dir.exists():
|
if case_dir.exists():
|
||||||
git_sync.commit_and_push(case_dir, f"טיוטת ביניים: {Path(path).name}")
|
git_sync.commit_and_push(case_dir, f"טיוטת ביניים: {Path(path).name}")
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"mode": "interim",
|
"mode": "interim",
|
||||||
"path": path,
|
"path": path,
|
||||||
"active_draft_path": path,
|
"active_draft_path": path,
|
||||||
"message": f"טיוטת ביניים נוצרה: {path}",
|
"message": f"טיוטת ביניים נוצרה: {path}",
|
||||||
}, ensure_ascii=False, indent=2)
|
})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def apply_user_edit(case_number: str, edit_filename: str) -> str:
|
async def apply_user_edit(case_number: str, edit_filename: str) -> str:
|
||||||
@@ -656,17 +635,13 @@ async def apply_user_edit(case_number: str, edit_filename: str) -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
export_dir = config.find_case_dir(case_number) / "exports"
|
export_dir = config.find_case_dir(case_number) / "exports"
|
||||||
edit_path = export_dir / edit_filename if "/" not in edit_filename else Path(edit_filename)
|
edit_path = export_dir / edit_filename if "/" not in edit_filename else Path(edit_filename)
|
||||||
if not edit_path.exists():
|
if not edit_path.exists():
|
||||||
return json.dumps({"status": "error",
|
return err(f"קובץ לא נמצא: {edit_path}")
|
||||||
"message": f"קובץ לא נמצא: {edit_path}"},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
retrofit_result = docx_retrofit.retrofit_bookmarks(edit_path)
|
retrofit_result = docx_retrofit.retrofit_bookmarks(edit_path)
|
||||||
@@ -675,17 +650,15 @@ async def apply_user_edit(case_number: str, edit_filename: str) -> str:
|
|||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
if case_dir.exists():
|
if case_dir.exists():
|
||||||
git_sync.commit_and_push(case_dir, f"גרסת עריכה: {edit_path.name}")
|
git_sync.commit_and_push(case_dir, f"גרסת עריכה: {edit_path.name}")
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"active_draft_path": str(edit_path),
|
"active_draft_path": str(edit_path),
|
||||||
"bookmarks_added": retrofit_result.get("bookmarks_added", []),
|
"bookmarks_added": retrofit_result.get("bookmarks_added", []),
|
||||||
"missing_blocks": retrofit_result.get("missing_blocks", []),
|
"missing_blocks": retrofit_result.get("missing_blocks", []),
|
||||||
"structural_fallback": retrofit_result.get("structural_fallback", []),
|
"structural_fallback": retrofit_result.get("structural_fallback", []),
|
||||||
"existing_bookmarks": retrofit_result.get("existing_bookmarks", []),
|
"existing_bookmarks": retrofit_result.get("existing_bookmarks", []),
|
||||||
}, ensure_ascii=False, indent=2)
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def list_bookmarks(case_number: str) -> str:
|
async def list_bookmarks(case_number: str) -> str:
|
||||||
@@ -697,26 +670,20 @@ async def list_bookmarks(case_number: str) -> str:
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
active_path = await db.get_active_draft_path(UUID(case["id"]))
|
active_path = await db.get_active_draft_path(UUID(case["id"]))
|
||||||
if not active_path or not Path(active_path).exists():
|
if not active_path or not Path(active_path).exists():
|
||||||
return json.dumps({"status": "no_active_draft",
|
return empty("לא נמצא active_draft. הרץ ייצוא או העלה עריכה.")
|
||||||
"message": "לא נמצא active_draft. הרץ ייצוא או העלה עריכה."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
names = docx_reviser.list_bookmarks(active_path)
|
names = docx_reviser.list_bookmarks(active_path)
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"active_draft_path": active_path,
|
"active_draft_path": active_path,
|
||||||
"bookmarks": names,
|
"bookmarks": names,
|
||||||
}, ensure_ascii=False, indent=2)
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def revise_draft(case_number: str, revisions_json: str,
|
async def revise_draft(case_number: str, revisions_json: str,
|
||||||
@@ -738,22 +705,17 @@ async def revise_draft(case_number: str, revisions_json: str,
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return json.dumps({"status": "error",
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
"message": f"תיק {case_number} לא נמצא."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
active_path = await db.get_active_draft_path(case_id)
|
active_path = await db.get_active_draft_path(case_id)
|
||||||
if not active_path or not Path(active_path).exists():
|
if not active_path or not Path(active_path).exists():
|
||||||
return json.dumps({"status": "error",
|
return err("אין active_draft. הרץ ייצוא או apply_user_edit קודם.")
|
||||||
"message": "אין active_draft. הרץ ייצוא או apply_user_edit קודם."},
|
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = json.loads(revisions_json) if isinstance(revisions_json, str) else revisions_json
|
raw = json.loads(revisions_json) if isinstance(revisions_json, str) else revisions_json
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
return json.dumps({"status": "error", "message": f"JSON לא תקף: {e}"},
|
return err(f"JSON לא תקף: {e}")
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
revisions = []
|
revisions = []
|
||||||
for item in raw:
|
for item in raw:
|
||||||
@@ -792,8 +754,7 @@ async def revise_draft(case_number: str, revisions_json: str,
|
|||||||
case_dir,
|
case_dir,
|
||||||
f"revise: טיוטה-v{next_ver} ({result.applied} שינויים, {result.failed} נכשלו)",
|
f"revise: טיוטה-v{next_ver} ({result.applied} שינויים, {result.failed} נכשלו)",
|
||||||
)
|
)
|
||||||
return json.dumps({
|
return ok({
|
||||||
"status": "completed",
|
|
||||||
"output_path": str(output_path),
|
"output_path": str(output_path),
|
||||||
"version": next_ver,
|
"version": next_ver,
|
||||||
"applied": result.applied,
|
"applied": result.applied,
|
||||||
@@ -803,10 +764,9 @@ async def revise_draft(case_number: str, revisions_json: str,
|
|||||||
{"id": r.id, "status": r.status, "error": r.error}
|
{"id": r.id, "status": r.status, "error": r.error}
|
||||||
for r in result.results
|
for r in result.results
|
||||||
],
|
],
|
||||||
}, ensure_ascii=False, indent=2)
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)},
|
return err(str(e))
|
||||||
ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_block_context(case_number: str, block_id: str, instructions: str = "") -> str:
|
async def get_block_context(case_number: str, block_id: str, instructions: str = "") -> str:
|
||||||
@@ -821,14 +781,14 @@ async def get_block_context(case_number: str, block_id: str, instructions: str =
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
try:
|
try:
|
||||||
ctx = await block_writer.get_block_context(case_id, block_id, instructions)
|
ctx = await block_writer.get_block_context(case_id, block_id, instructions)
|
||||||
return json.dumps(ctx, default=str, ensure_ascii=False, indent=2)
|
return ok(ctx)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return str(e)
|
return err(str(e))
|
||||||
|
|
||||||
|
|
||||||
async def save_block_content(case_number: str, block_id: str, content: str) -> str:
|
async def save_block_content(case_number: str, block_id: str, content: str) -> str:
|
||||||
@@ -843,14 +803,14 @@ async def save_block_content(case_number: str, block_id: str, content: str) -> s
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
try:
|
try:
|
||||||
result = await block_writer.save_block_content(case_id, block_id, content)
|
result = await block_writer.save_block_content(case_id, block_id, content)
|
||||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return str(e)
|
return err(str(e))
|
||||||
|
|
||||||
|
|
||||||
async def analyze_style(appeal_subtype: str = "") -> str:
|
async def analyze_style(appeal_subtype: str = "") -> str:
|
||||||
@@ -863,7 +823,7 @@ async def analyze_style(appeal_subtype: str = "") -> str:
|
|||||||
from legal_mcp.services.style_analyzer import analyze_corpus
|
from legal_mcp.services.style_analyzer import analyze_corpus
|
||||||
|
|
||||||
result = await analyze_corpus(appeal_subtype)
|
result = await analyze_corpus(appeal_subtype)
|
||||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
|
|
||||||
|
|
||||||
async def write_block(
|
async def write_block(
|
||||||
@@ -882,15 +842,15 @@ async def write_block(
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await block_writer.write_and_store_block(case_id, block_id, instructions)
|
result = await block_writer.write_and_store_block(case_id, block_id, instructions)
|
||||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
return ok(result)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return str(e)
|
return err(str(e))
|
||||||
|
|
||||||
|
|
||||||
async def write_all_blocks(
|
async def write_all_blocks(
|
||||||
@@ -910,7 +870,7 @@ async def write_all_blocks(
|
|||||||
|
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return err(f"תיק {case_number} לא נמצא.")
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
@@ -944,8 +904,8 @@ async def write_all_blocks(
|
|||||||
break
|
break
|
||||||
|
|
||||||
total_words = sum(r.get("word_count", 0) for r in results)
|
total_words = sum(r.get("word_count", 0) for r in results)
|
||||||
return json.dumps({
|
return ok({
|
||||||
"blocks": results,
|
"blocks": results,
|
||||||
"total_words": total_words,
|
"total_words": total_words,
|
||||||
"completed": sum(1 for r in results if r["status"] == "completed"),
|
"completed": sum(1 for r in results if r["status"] == "completed"),
|
||||||
}, default=str, ensure_ascii=False, indent=2)
|
})
|
||||||
|
|||||||
@@ -125,8 +125,9 @@ def test_export_blocked_when_critical_failures(
|
|||||||
monkeypatch.setattr(db, "get_critical_qa_failures", _get_critical)
|
monkeypatch.setattr(db, "get_critical_qa_failures", _get_critical)
|
||||||
|
|
||||||
out = json.loads(_run(drafting.export_docx("8001-24")))
|
out = json.loads(_run(drafting.export_docx("8001-24")))
|
||||||
|
# GAP-48: {status,data,message} envelope; failed_gates rides in data.
|
||||||
assert out["status"] == "error"
|
assert out["status"] == "error"
|
||||||
assert out["failed_gates"] == ["claims_coverage", "structural_integrity"]
|
assert out["data"]["failed_gates"] == ["claims_coverage", "structural_integrity"]
|
||||||
assert "claims_coverage" in out["message"]
|
assert "claims_coverage" in out["message"]
|
||||||
assert patched_export["exported"] is False, "must not call the exporter"
|
assert patched_export["exported"] is False, "must not call the exporter"
|
||||||
assert patched_export["committed"] is False, "must not git-commit"
|
assert patched_export["committed"] is False, "must not git-commit"
|
||||||
@@ -145,7 +146,8 @@ def test_export_proceeds_when_clean(
|
|||||||
monkeypatch.setattr(db, "get_critical_qa_failures", _get_critical)
|
monkeypatch.setattr(db, "get_critical_qa_failures", _get_critical)
|
||||||
|
|
||||||
out = json.loads(_run(drafting.export_docx("8001-24")))
|
out = json.loads(_run(drafting.export_docx("8001-24")))
|
||||||
assert out["status"] == "completed", out
|
# GAP-48: success is envelope status "ok"; payload (path) rides in data.
|
||||||
assert out["path"] == "/tmp/decision.docx"
|
assert out["status"] == "ok", out
|
||||||
|
assert out["data"]["path"] == "/tmp/decision.docx"
|
||||||
assert patched_export["exported"] is True, "clean QA must allow export"
|
assert patched_export["exported"] is True, "clean QA must allow export"
|
||||||
assert patched_export["set_draft"] is True, "active_draft_path must be set"
|
assert patched_export["set_draft"] is True, "active_draft_path must be set"
|
||||||
|
|||||||
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):
|
async def api_case_template(case_number: str):
|
||||||
"""Get outcome-aware decision template for a case."""
|
"""Get outcome-aware decision template for a case."""
|
||||||
result = await drafting_tools.get_decision_template(case_number)
|
result = await drafting_tools.get_decision_template(case_number)
|
||||||
if result.startswith("תיק"):
|
parsed = json.loads(result) # GAP-48
|
||||||
raise HTTPException(404, result)
|
if parsed.get("status") == "error":
|
||||||
return {"template": result}
|
raise HTTPException(404, parsed.get("message") or "")
|
||||||
|
return {"template": envelope_unwrap(parsed)}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/processing-status")
|
@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"}
|
auto_result: dict = {"status": "ok"}
|
||||||
try:
|
try:
|
||||||
raw = await drafting_tools.apply_user_edit(case_number, dest.name)
|
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:
|
except Exception as e:
|
||||||
auto_result = {"status": "error", "message": str(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),
|
json.dumps(req.revisions, ensure_ascii=False),
|
||||||
req.author,
|
req.author,
|
||||||
)
|
)
|
||||||
try:
|
parsed = json.loads(raw) # GAP-48
|
||||||
data = json.loads(raw)
|
if parsed.get("status") == "error":
|
||||||
except json.JSONDecodeError:
|
raise HTTPException(400, parsed.get("message", "revise failed"))
|
||||||
raise HTTPException(500, raw)
|
return envelope_unwrap(parsed)
|
||||||
if data.get("status") == "error":
|
|
||||||
raise HTTPException(400, data.get("message", "revise failed"))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/cases/{case_number}/exports/bookmarks")
|
@app.get("/api/cases/{case_number}/exports/bookmarks")
|
||||||
async def api_list_bookmarks(case_number: str):
|
async def api_list_bookmarks(case_number: str):
|
||||||
"""List bookmarks in the case's active draft (anchors for revisions)."""
|
"""List bookmarks in the case's active draft (anchors for revisions)."""
|
||||||
raw = await drafting_tools.list_bookmarks(case_number)
|
raw = await drafting_tools.list_bookmarks(case_number)
|
||||||
try:
|
parsed = json.loads(raw) # GAP-48
|
||||||
data = json.loads(raw)
|
data = envelope_unwrap(parsed)
|
||||||
except json.JSONDecodeError:
|
return data if isinstance(data, dict) else {"bookmarks": []}
|
||||||
raise HTTPException(500, raw)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/cases/{case_number}/exports/{filename}/retrofit")
|
@app.post("/api/cases/{case_number}/exports/{filename}/retrofit")
|
||||||
async def api_retrofit_bookmarks(case_number: str, filename: str):
|
async def api_retrofit_bookmarks(case_number: str, filename: str):
|
||||||
"""Manually trigger retrofit of bookmarks on an existing file."""
|
"""Manually trigger retrofit of bookmarks on an existing file."""
|
||||||
raw = await drafting_tools.apply_user_edit(case_number, filename)
|
raw = await drafting_tools.apply_user_edit(case_number, filename)
|
||||||
try:
|
parsed = json.loads(raw) # GAP-48
|
||||||
data = json.loads(raw)
|
if parsed.get("status") == "error":
|
||||||
except json.JSONDecodeError:
|
raise HTTPException(400, parsed.get("message", "retrofit failed"))
|
||||||
raise HTTPException(500, raw)
|
return envelope_unwrap(parsed)
|
||||||
if data.get("status") == "error":
|
|
||||||
raise HTTPException(400, data.get("message", "retrofit failed"))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/cases/{case_number}/active-draft")
|
@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.
|
(markdown body + download link) to the linked issue.
|
||||||
"""
|
"""
|
||||||
result = await drafting_tools.export_docx(case_number)
|
result = await drafting_tools.export_docx(case_number)
|
||||||
if isinstance(result, dict):
|
parsed = json.loads(result) # GAP-48: tool returns the {status,data,message} envelope
|
||||||
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))
|
|
||||||
|
|
||||||
# FU-6: a QA gate (or another error) can block the export. export_docx
|
# FU-6: a QA gate (or another error) can block the export. export_docx
|
||||||
# signals this with status == "error". Returning the existing 200 here
|
# signals this with envelope status == "error". Returning the existing 200
|
||||||
# would let the UI show a false "exported successfully" toast, so we map
|
# 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.
|
# a block to 409 Conflict carrying the Hebrew message + failed_gates (in data).
|
||||||
if isinstance(data, dict) and data.get("status") == "error":
|
if parsed.get("status") == "error":
|
||||||
detail = {"message": data.get("message", "ייצוא נחסם.")}
|
detail = {"message": parsed.get("message", "ייצוא נחסם.")}
|
||||||
if data.get("failed_gates"):
|
inner = parsed.get("data") or {}
|
||||||
detail["failed_gates"] = data["failed_gates"]
|
if inner.get("failed_gates"):
|
||||||
|
detail["failed_gates"] = inner["failed_gates"]
|
||||||
raise HTTPException(409, detail)
|
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.
|
# Notify the Paperclip plugin to attach the final-decision document.
|
||||||
docx_filename = (
|
docx_filename = (
|
||||||
data.get("filename")
|
data.get("filename")
|
||||||
|
|||||||
Reference in New Issue
Block a user