feat(citation-verify): backend for the "אימות פסיקה" panel — per-argument support + verify gate (#154)
Backend half of the citation-verification tab (frontend follows in a separate PR):
- Schema V44: case_precedents gains argument_id (the legal argument it supports),
case_law_id (the corpus ruling — for cited_by + dedup), verified (the INV-AH gate
the writer respects) + verified_at. All nullable; chair_note already existed.
- db: create_case_precedent(+argument_id/case_law_id/verified),
set_case_precedent_verified(id, verified, chair_note), list_case_precedents(+cols).
- services/case_citation_verification.build_view(case): per legal_argument →
in-corpus suggestions (search_library per issue) each with the cited_by authority
breakdown (db.citation_authority, X11) + merged attach/verify state + per-issue
radar (case_digest_radar grouped by matched issue). Pure read/assembly; reuses the
one corpus search + one authority query + one radar (G2).
- endpoints: GET /api/cases/{n}/citation-verification (the view) and
POST .../verify (upsert verify/un-verify + chair note).
Validated on 1044-03-26: 14 arguments, all with support; 317/10 surfaces with
אומץ×11/אובחן×1; radar leads attributed per issue. Auto-approval untouched (chair
gate, INV-G10). Verify defaults to false — nothing is authoritative until the chair
marks it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
web/app.py
56
web/app.py
@@ -3155,6 +3155,62 @@ async def api_precedent_list(case_number: str):
|
||||
return envelope_unwrap(parsed)
|
||||
|
||||
|
||||
# ── Citation-verification panel (X11 Phase 2 / #154) — "אימות פסיקה" tab ──
|
||||
|
||||
|
||||
@app.get("/api/cases/{case_number}/citation-verification")
|
||||
async def api_citation_verification(case_number: str):
|
||||
"""Per legal-argument: supporting corpus precedents (with cited_by authority),
|
||||
their verify state + chair note, and per-issue radar (unlinked digests). Powers
|
||||
the compose 'אימות פסיקה' tab — the chair verifies before the writer cites."""
|
||||
from legal_mcp.services import case_citation_verification as ccv
|
||||
view = await ccv.build_view(case_number)
|
||||
if view.get("status") == "case_not_found":
|
||||
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||
return view
|
||||
|
||||
|
||||
class CitationVerifyRequest(BaseModel):
|
||||
argument_id: str
|
||||
case_law_id: str = ""
|
||||
quote: str = ""
|
||||
citation: str = ""
|
||||
chair_note: str | None = None
|
||||
verified: bool = True
|
||||
attached_id: str = "" # set when the precedent row already exists
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/citation-verification/verify")
|
||||
async def api_citation_verify(case_number: str, req: CitationVerifyRequest):
|
||||
"""Verify / un-verify a precedent for a specific argument (the INV-AH gate the
|
||||
writer respects). Upsert: PATCH an existing attachment, else attach a new one
|
||||
linked to the argument + corpus ruling and mark it."""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||
try:
|
||||
if req.attached_id:
|
||||
row = await db.set_case_precedent_verified(
|
||||
UUID(req.attached_id), req.verified, req.chair_note)
|
||||
if not row:
|
||||
raise HTTPException(404, "שיוך-פסיקה לא נמצא")
|
||||
return row
|
||||
if not req.quote.strip() or not req.citation.strip():
|
||||
raise HTTPException(400, "quote ו-citation חובה לצירוף חדש")
|
||||
cid = case["id"]
|
||||
row = await db.create_case_precedent(
|
||||
case_id=UUID(cid) if isinstance(cid, str) else cid,
|
||||
quote=req.quote, citation=req.citation,
|
||||
chair_note=req.chair_note or "",
|
||||
argument_id=UUID(req.argument_id) if req.argument_id else None,
|
||||
case_law_id=UUID(req.case_law_id) if req.case_law_id else None,
|
||||
verified=req.verified,
|
||||
)
|
||||
return row
|
||||
except ValueError:
|
||||
raise HTTPException(400, "מזהה לא תקין")
|
||||
|
||||
|
||||
@app.delete("/api/precedents/{precedent_id}")
|
||||
async def api_precedent_delete(precedent_id: str):
|
||||
"""Delete a precedent attachment. The archived PDF (if any) stays
|
||||
|
||||
Reference in New Issue
Block a user