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 ( - +