"""Citation-verification view (X11 Phase 2 / #154) — the chair's "אימות פסיקה" tab. Assembles, per legal ARGUMENT of a case, the supporting precedents the chair should verify before the writer cites them: • in-corpus suggestions — per-issue semantic retrieval over the authoritative precedent library (``search_library``), each carrying the cumulative authority signal (``cited_by``: followed/distinguished — db.citation_authority, X11). • attached/verified state — any ``case_precedents`` row already attached to the argument (verified flag + chair_note), merged onto the matching suggestion. • radar — UNLINKED digests relevant to the same issue (rulings we don't hold yet), from ``case_digest_radar`` grouped by matched issue. Pure read/assembly — never writes, never cites (INV-DIG1/INV-AH). The chair verifies through ``db.set_case_precedent_verified`` / attach; the writer consumes only verified rows. Reuses the one corpus search + the one authority query + the one radar (G2). """ from __future__ import annotations import logging from uuid import UUID from legal_mcp.services import ( argument_aggregator, db, digest_library, precedent_library, ) logger = logging.getLogger(__name__) _SUGGEST_PER_ISSUE = 4 _SUGGEST_FLOOR = 0.45 async def build_view(case_number: str) -> dict: case = await db.get_case_by_number(case_number) if not case: return {"status": "case_not_found", "case_number": case_number, "arguments": []} case_id = case["id"] if isinstance(case_id, str): case_id = UUID(case_id) ctx = " ".join(x for x in [case.get("title") or "", case.get("appeal_subtype") or ""] if x).strip() args = await argument_aggregator.get_legal_arguments(case_id) # Attached precedents already on the case → grouped by argument_id, keyed by # the resolved corpus ruling so we can merge verify-state onto a suggestion. attached = await db.list_case_precedents(case_id) attached_by_arg: dict[str, dict[str, dict]] = {} for p in attached: aid = str(p.get("argument_id") or "") clid = str(p.get("case_law_id") or "") if aid and clid: attached_by_arg.setdefault(aid, {})[clid] = p # Radar (unlinked digests) once, grouped by the issue label it matched. radar_by_issue: dict[str, list[dict]] = {} try: radar = await digest_library.case_digest_radar(case_number, limit=12, min_score=0.42) for lead in radar.get("leads", []): for label in (lead.get("matched_issues") or ["—"]): radar_by_issue.setdefault(label, []).append(lead) except Exception as e: # noqa: BLE001 — radar is best-effort logger.warning("citation_verification radar failed for %s: %s", case_number, e) out_args: list[dict] = [] n_verified = 0 for a in args: aid = str(a["id"]) title = (a.get("argument_title") or "").strip() topic = (a.get("legal_topic") or "").strip() query = f"{ctx} {title}. {topic}".strip() hits = [] try: hits = await precedent_library.search_library( query=query, limit=_SUGGEST_PER_ISSUE, include_halachot=True) except Exception as e: # noqa: BLE001 logger.warning("citation_verification search failed (%s): %s", title[:30], e) # Resolve the authority breakdown for the hit set in one batched query. clids = [UUID(str(h["case_law_id"])) for h in hits if h.get("case_law_id") and float(h.get("score", 0) or 0) >= _SUGGEST_FLOOR] authority = await db.citation_authority(clids) if clids else {} seen: set[str] = set() supporting: list[dict] = [] for h in hits: clid = str(h.get("case_law_id") or "") if not clid or clid in seen: continue if float(h.get("score", 0) or 0) < _SUGGEST_FLOOR: continue seen.add(clid) att = attached_by_arg.get(aid, {}).get(clid) if att and att.get("verified"): n_verified += 1 supporting.append({ "case_law_id": clid, "case_number": h.get("case_number") or "", "case_name": h.get("case_name") or "", "quote": h.get("supporting_quote") or h.get("rule_statement") or "", "score": round(float(h.get("score", 0) or 0), 3), "cited_by": authority.get(clid, {"total": 0, "positive": 0, "negative": 0, "unclassified": 0, "by_treatment": {}}), "attached_id": str(att["id"]) if att else None, "verified": bool(att.get("verified")) if att else False, "chair_note": (att.get("chair_note") or "") if att else "", }) out_args.append({ "argument_id": aid, "title": title, "legal_topic": topic, "priority": a.get("priority") or "", "party": a.get("party") or "", "supporting": supporting, "radar": radar_by_issue.get(title, []), }) return { "status": "ok", "case_number": case_number, "arguments": out_args, "summary": { "arguments_total": len(out_args), "arguments_with_support": sum(1 for x in out_args if x["supporting"]), "verified": n_verified, "radar_leads": sum(len(x["radar"]) for x in out_args), }, }