From 91c521922f9775fc1ec23fe622cb9a3d2a4ecc8e Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 20 Jun 2026 14:54:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(precedents):=20surface=20auto-detected=20i?= =?UTF-8?q?ncoming=20citations=20in=20the=20"=D7=A6=D7=99=D7=98=D7=95?= =?UTF-8?q?=D7=98=D7=99=D7=9D=20=D7=9E=D7=A7=D7=95=D7=A9=D7=A8=D7=99=D7=9D?= =?UTF-8?q?"=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The precedent-detail "ציטוטים מקושרים" panel rendered only MANUAL appeal-chain links (case_law_relations), so it stayed empty even when decisions cite the ruling — the automatic citation graph (precedent_internal_citations) was never surfaced. e.g. עע"מ 317/10 (שפר) has 12 incoming citations in the DB, none shown. Fills the existing panel from the citation graph (data/logic only, no new visual design — gate-exempt per web-ui/AGENTS.md): - citation_extractor.list_citations_to_case_law: return source precedent_level/ court/date too (additive; reuses the one canonical incoming query — G2). - precedent_library.get_precedent: add incoming_citations[], shaped like the RelatedCase row. No parallel resolution path. - web-ui: render incoming citations in the same row style, read-only (no unlink), de-duped against manual links; manual "קשר" linking preserved alongside. Invariants: G2 (single citation-resolution path, reused), G1 (graph self-heals via existing relink_orphan_citations on upload). No schema change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../legal_mcp/services/citation_extractor.py | 5 +- .../legal_mcp/services/precedent_library.py | 22 ++++++++ web-ui/src/app/precedents/[id]/page.tsx | 6 ++- .../precedents/link-related-dialog.tsx | 54 +++++++++++++++++-- web-ui/src/lib/api/precedent-library.ts | 14 +++++ 5 files changed, 95 insertions(+), 6 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/citation_extractor.py b/mcp-server/src/legal_mcp/services/citation_extractor.py index 323f5d8..0368a93 100644 --- a/mcp-server/src/legal_mcp/services/citation_extractor.py +++ b/mcp-server/src/legal_mcp/services/citation_extractor.py @@ -404,7 +404,10 @@ async def list_citations_to_case_law(case_law_id: UUID) -> list[dict]: cl.case_number AS source_case_number, cl.case_name AS source_case_name, cl.chair_name AS source_chair_name, - cl.district AS source_district + cl.district AS source_district, + cl.precedent_level AS source_precedent_level, + cl.court AS source_court, + cl.date AS source_date FROM precedent_internal_citations pic JOIN case_law cl ON cl.id = pic.source_case_law_id WHERE pic.cited_case_law_id = $1 diff --git a/mcp-server/src/legal_mcp/services/precedent_library.py b/mcp-server/src/legal_mcp/services/precedent_library.py index d153000..8fca14a 100644 --- a/mcp-server/src/legal_mcp/services/precedent_library.py +++ b/mcp-server/src/legal_mcp/services/precedent_library.py @@ -411,6 +411,28 @@ async def get_precedent(case_law_id: UUID | str) -> dict | None: return None record["halachot"] = await db.list_halachot(case_law_id=case_law_id, limit=500) record["related_cases"] = await db.get_case_law_relations(case_law_id) + # Auto-detected citation graph: which decisions cite THIS ruling (incoming + # edges in precedent_internal_citations). Reuses the canonical query (G2) — + # no parallel resolution path. Shaped to match the front-end RelatedCase row + # so the existing "ציטוטים מקושרים" panel renders them with no visual change. + from legal_mcp.services import citation_extractor as _ce + raw = await _ce.list_citations_to_case_law(case_law_id) + record["incoming_citations"] = [ + { + "id": r["source_case_law_id"], + "case_number": r.get("source_case_number") or r.get("cited_case_number") or "", + "case_name": r.get("source_case_name") or "", + "court": r.get("source_court") or "", + "precedent_level": r.get("source_precedent_level") or "", + "chair_name": r.get("source_chair_name") or "", + "date": ( + r["source_date"].isoformat() + if r.get("source_date") is not None else None + ), + "confidence": r.get("confidence"), + } + for r in raw + ] return record diff --git a/web-ui/src/app/precedents/[id]/page.tsx b/web-ui/src/app/precedents/[id]/page.tsx index 768932e..8c12d4d 100644 --- a/web-ui/src/app/precedents/[id]/page.tsx +++ b/web-ui/src/app/precedents/[id]/page.tsx @@ -236,7 +236,11 @@ export default function PrecedentDetailPage({ {/* side rail — citations + corroboration */}
- +
diff --git a/web-ui/src/components/precedents/link-related-dialog.tsx b/web-ui/src/components/precedents/link-related-dialog.tsx index a123f27..911752c 100644 --- a/web-ui/src/components/precedents/link-related-dialog.tsx +++ b/web-ui/src/components/precedents/link-related-dialog.tsx @@ -18,6 +18,7 @@ import { useLinkRelatedCase, useUnlinkRelatedCase, RelatedCase, + IncomingCitation, } from "@/lib/api/precedent-library"; const LEVEL_LABELS: Record = { @@ -133,19 +134,28 @@ function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps) type SectionProps = { caseId: string; related: RelatedCase[]; + /** Decisions that cite THIS ruling — auto-detected from the citation graph + * (read-only). Optional so existing call-sites stay valid. */ + incoming?: IncomingCitation[]; }; /* Rail-styled citations card (mockup 08 side rail). Renders linked related * decisions as a navy-headed card with arrow-prefixed rows; keeps the full - * link/unlink logic. Used in the precedent-detail side rail. */ -export function RelatedCasesSection({ caseId, related }: SectionProps) { + * link/unlink logic. Auto-detected incoming citations (decisions that cite this + * ruling) render in the SAME row style, read-only (no unlink). Used in the + * precedent-detail side rail. */ +export function RelatedCasesSection({ caseId, related, incoming = [] }: SectionProps) { const [dialogOpen, setDialogOpen] = useState(false); + // De-dupe: a decision already linked manually shouldn't appear twice. + const manualIds = new Set(related.map((r) => r.id)); + const autoCites = incoming.filter((c) => !manualIds.has(c.id)); + const total = related.length + autoCites.length; return (

- ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""} + ציטוטים מקושרים{total > 0 ? ` (${total})` : ""}

- {related.length === 0 ? ( + {total === 0 ? (

אין החלטות קשורות עדיין

) : (