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:
@@ -105,6 +105,11 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- INV-TOOL3 / GAP-52: SHA-256 of the uploaded file bytes, for idempotent upload
|
||||
-- (re-uploading the same file to a case returns the existing document). Empty
|
||||
-- default = legacy rows with unknown hash; never matched as a duplicate.
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS content_hash text NOT NULL DEFAULT '';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
|
||||
@@ -1471,19 +1476,37 @@ async def create_document(
|
||||
title: str,
|
||||
file_path: str,
|
||||
page_count: int | None = None,
|
||||
content_hash: str = "",
|
||||
) -> dict:
|
||||
pool = await get_pool()
|
||||
doc_id = uuid4()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""INSERT INTO documents (id, case_id, doc_type, title, file_path, page_count)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
doc_id, case_id, doc_type, title, file_path, page_count,
|
||||
"""INSERT INTO documents (id, case_id, doc_type, title, file_path, page_count, content_hash)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
|
||||
doc_id, case_id, doc_type, title, file_path, page_count, content_hash,
|
||||
)
|
||||
row = await conn.fetchrow("SELECT * FROM documents WHERE id = $1", doc_id)
|
||||
return _row_to_doc(row)
|
||||
|
||||
|
||||
async def get_document_by_hash(case_id: UUID, content_hash: str) -> dict | None:
|
||||
"""Return an existing document for this case with the same file hash, or None.
|
||||
|
||||
INV-TOOL3 / GAP-52: deterministic key for idempotent upload. Empty hashes
|
||||
(legacy rows) are never matched.
|
||||
"""
|
||||
if not content_hash:
|
||||
return None
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"SELECT * FROM documents WHERE case_id = $1 AND content_hash = $2 LIMIT 1",
|
||||
case_id, content_hash,
|
||||
)
|
||||
return _row_to_doc(row) if row else None
|
||||
|
||||
|
||||
async def update_document(doc_id: UUID, **fields) -> None:
|
||||
if not fields:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user