From 2962538c0964786e8f00efd6b02d193c10c1444d Mon Sep 17 00:00:00 2001 From: Chaim Date: Fri, 12 Jun 2026 09:03:29 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=D7=AA=D7=99=D7=A7=D7=95=D7=9F-=D7=A6?= =?UTF-8?q?=D7=99=D7=98=D7=95=D7=98=20=D7=91=D7=93=D7=9C=D7=99-=D7=94?= =?UTF-8?q?=D7=97=D7=99=D7=9C=D7=95=D7=A5=20+=20=D7=A7=D7=99=D7=A9=D7=95?= =?UTF-8?q?=D7=A8-=D7=9C=D7=AA=D7=95=D7=A8=20=D7=9E=D7=93=D7=A3-=D7=A4?= =?UTF-8?q?=D7=A8=D7=98=20(#133=20follow-ups)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit אושר ב-Claude Design (כרטיס 20-halacha-followups). א׳ תיקון-חילוץ אמיתי ל-quote_unverified: - `update_halacha` מקבל `supporting_quote`; בעדכונו מריץ `_verify_quote` הקיים מול `case_law.full_text` השמור (דטרמיניסטי — בלי OCR/LLM מחדש, feedback_no_reocr_retrofit) ומסנכרן `quote_verified` + מוסיף/מסיר את הדגל `quote_unverified`. יו"ר שמדביק את הנוסח הנכון מהמקור → הדגל נמחק → ההלכה עוזבת את דלי-החילוץ. `HalachaUpdateRequest`+handler מעבירים את השדה; `HalachaPatch` + מצב-העריכה ב-HalachaCard כוללים textarea-ציטוט (נשלח רק כששונה) + hint. ב׳ דף-פרט פסיקה — ביטול כפילות-המשטח: - הלכה pending ב-`ExtractedHalachotSection` מציגה קישור "עבור לתור הלכות" במקום כפתורי אשר/דחה כפולים (שער-אישור יחיד, INV-IA/G10). - `/precedents` Tabs הפך נשלט וקורא `?tab=review` (post-mount, בלי hydration-mismatch) כדי שהקישור ינחת על טאב-התור. display-only ל-G10 (האימות מסנכרן מטא-איכות, לא review_status). ולידציה: py_compile + tsc + eslint נקיים. Co-Authored-By: Claude Opus 4.8 --- mcp-server/src/legal_mcp/services/db.py | 30 +++++++++++++- web-ui/src/app/precedents/page.tsx | 15 ++++++- .../precedents/extracted-halachot.tsx | 31 +++++++------- .../precedents/halacha-review-panel.tsx | 40 ++++++++++++++++--- web-ui/src/lib/api/precedent-library.ts | 3 ++ web/app.py | 2 + 6 files changed, 96 insertions(+), 25 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 8cb9275..59b5b7a 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -4658,12 +4658,40 @@ async def update_halacha( reasoning_summary: str | None = None, subject_tags: list[str] | None = None, practice_areas: list[str] | None = None, + supporting_quote: str | None = None, ) -> dict | None: - """Update a halacha — used by the chair to approve/reject/edit.""" + """Update a halacha — used by the chair to approve/reject/edit. + + #133 follow-up — extraction repair: when ``supporting_quote`` is edited it is + RE-VERIFIED against the stored source text (deterministic; no re-OCR / no LLM — + see feedback_no_reocr_retrofit) and ``quote_verified`` + the ``quote_unverified`` + quality flag are synced. A chair who pastes the correct quote from the source + clears the flag, and the item leaves the 'fix' bucket.""" pool = await get_pool() set_parts: list[str] = [] params: list = [halacha_id] idx = 2 + if supporting_quote is not None: + set_parts.append(f"supporting_quote = ${idx}") + params.append(supporting_quote) + idx += 1 + # re-verify against the stored full_text, sync quote_verified + the flag + src = await pool.fetchrow( + "SELECT cl.full_text, h.quality_flags FROM halachot h " + "JOIN case_law cl ON cl.id = h.case_law_id WHERE h.id = $1", halacha_id) + if src is not None: + from legal_mcp.services.halacha_extractor import _verify_quote + verified = _verify_quote(supporting_quote, src["full_text"] or "") + set_parts.append(f"quote_verified = ${idx}") + params.append(verified) + idx += 1 + flags = [f for f in (src["quality_flags"] or []) + if f != halacha_quality.FLAG_QUOTE_UNVERIFIED] + if not verified: + flags.append(halacha_quality.FLAG_QUOTE_UNVERIFIED) + set_parts.append(f"quality_flags = ${idx}") + params.append(flags) + idx += 1 if review_status is not None: set_parts.append(f"review_status = ${idx}") params.append(review_status) diff --git a/web-ui/src/app/precedents/page.tsx b/web-ui/src/app/precedents/page.tsx index 3a2c64a..460a20c 100644 --- a/web-ui/src/app/precedents/page.tsx +++ b/web-ui/src/app/precedents/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AppShell } from "@/components/app-shell"; @@ -52,10 +53,22 @@ function IncomingPill() { return ; } +const PRECEDENT_TABS = new Set(["library", "search", "review", "incoming", "stats"]); + export default function PrecedentsPage() { + // Controlled so a deep link like /precedents?tab=review (e.g. from a pending + // halacha on a precedent-detail page, #133) lands on the right tab. Read after + // mount to avoid an SSR/CSR mismatch and the useSearchParams Suspense rule. + const [tab, setTab] = useState("library"); + useEffect(() => { + // read post-mount (not lazy init) to avoid an SSR/CSR hydration mismatch + const t = new URLSearchParams(window.location.search).get("tab"); + // eslint-disable-next-line react-hooks/set-state-in-effect + if (t && PRECEDENT_TABS.has(t)) setTab(t); + }, []); return ( - +