feat(mcp): FU-14 GAP-52 — idempotency על case_create/precedent_attach/document_upload
INV-TOOL3 (idempotency על מפתח דטרמיניסטי). כל שלושת הכלים מחזירים את הרשומה הקיימת במקום ליצור כפילות: - case_create — מפתח case_number (כבר UNIQUE ב-schema): מחזיר את התיק הקיים במקום unique-violation. - precedent_attach — מפתח (case_id, section_id, citation, quote): צירוף חוזר של אותו ציטוט לאותו סעיף מחזיר את הקיים. - document_upload — מפתח (case_id, SHA-256 של בייטי הקובץ): העלאה חוזרת של אותו קובץ מחזירה את המסמך הקיים ו**מדלגת על copy+OCR+embed** (החלק היקר). נוספה עמודת documents.content_hash (תוספתי, DEFAULT '') + get_document_by_hash. נבחרה בדיקת-מפתח ברמת-אפליקציה (SELECT-לפני-INSERT) ולא UNIQUE-constraint — כדי לא לשבור startup אם קיימים נתונים-כפולים legacy. אין מיגרציה הרסנית. עודכנו docs/spec/X9 (INV-TOOL3 ✅) ו-gap-audit (GAP-52 ✅, פרוסה 2). py_compile עבר על 4 קבצי הקוד. אימות runtime (restart MCP server) נדחה עד שהחילוץ הפעיל יסתיים. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,13 @@ async def case_create(
|
||||
ריק = יוסק אוטומטית ממספר התיק
|
||||
proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject.
|
||||
"""
|
||||
# INV-TOOL3 / GAP-52: idempotent on case_number (already UNIQUE in schema).
|
||||
# Re-creating an existing case returns it instead of raising a unique-violation.
|
||||
_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)
|
||||
|
||||
from datetime import date as date_type
|
||||
|
||||
h_date = None
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
@@ -37,6 +38,19 @@ async def document_upload(
|
||||
if not title:
|
||||
title = source.stem
|
||||
|
||||
# INV-TOOL3 / GAP-52: idempotent on (case_id, file content hash). Re-uploading
|
||||
# the same bytes returns the existing document and skips re-copy + re-OCR +
|
||||
# re-embed (the expensive part).
|
||||
content_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
existing_doc = await db.get_document_by_hash(case_id, content_hash)
|
||||
if existing_doc:
|
||||
return json.dumps({
|
||||
"status": "exists",
|
||||
"message": f"הקובץ כבר הועלה לתיק {case_number} (זהה ב-hash) — מוחזר הקיים, ללא עיבוד מחדש.",
|
||||
"document": existing_doc,
|
||||
"idempotent_existing": True,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
# Copy file to case directory
|
||||
case_dir = config.find_case_dir(case_number) / "documents" / "originals"
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -52,6 +66,7 @@ async def document_upload(
|
||||
doc_type=initial_doc_type,
|
||||
title=title,
|
||||
file_path=str(dest),
|
||||
content_hash=content_hash,
|
||||
)
|
||||
|
||||
# Process document (extract → classify → chunk → embed → store)
|
||||
|
||||
@@ -43,6 +43,14 @@ async def precedent_attach(
|
||||
except ValueError:
|
||||
return json.dumps({"error": "pdf_document_id לא תקין"}, ensure_ascii=False)
|
||||
|
||||
# INV-TOOL3 / GAP-52: idempotent on (case_id, section_id, citation, quote).
|
||||
# Re-attaching the same quote to the same section returns the existing row.
|
||||
for _p in await db.list_case_precedents(UUID(case["id"])):
|
||||
if (_p.get("citation") == citation and _p.get("quote") == quote
|
||||
and (_p.get("section_id") or None) == (section_id or None)):
|
||||
_p["idempotent_existing"] = True
|
||||
return json.dumps(_p, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
row = await db.create_case_precedent(
|
||||
case_id=UUID(case["id"]),
|
||||
quote=quote,
|
||||
|
||||
Reference in New Issue
Block a user