From a18ed8ffb74920adc4f540c1b5610dea63419993 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 20 Jun 2026 12:28:38 +0000 Subject: [PATCH] feat(missing-precedents): bridge cited-by-chair + decision number; open/closed tabs; single-line citation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the chair-approved redesign of /missing-precedents (Claude Design card 09). The "צוטט ע״י" and "תיק" columns were empty for ~98% of rows because the corpus-decision reliance lives in precedent_internal_citations (keyed to case_law), a different system than missing_precedents (keyed to cases). Backend (G2 — read-time bridge, no stored duplication): - list_missing_precedents: new cited_by_chairs / cited_by_decisions columns, resolved from the citation graph by normalized docket number (same normalization the relinker uses). For an open gap cited by committee decisions, surfaces the chair(s) (e.g. דפנה תמיר) + the deciding case numbers. Frontend (matches approved mockup): - "צוטט ע״י" shows the chair chip (bridge) with priority over the generic discovery-source chips; "תיק" shows the deciding decision number(s) + "+N". - Status filter reduced to פתוח / נסגר (removed הועלה, לא-רלוונטי, הכל). - "פסיקה" column collapses to a single line when case_name is empty — fixes the duplicated-citation two-row rendering. - Lifecycle note simplified to open → closed. Verified: bridge SQL returns chair+decisions on live data; tsc --noEmit clean; py_compile clean. Follow-up (data, separate): LLM backfill of legal_topic (נושא); create missing_precedents rows for the 28 graph-cited rulings not yet tracked. Invariants: G2 (single source of truth — bridge derived at read, no duplicated column) · INV-AH context (closing source gaps). Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp-server/src/legal_mcp/services/db.py | 28 ++++++++- web-ui/src/app/missing-precedents/page.tsx | 10 ++-- .../missing-precedents-table.tsx | 59 ++++++++++++++++--- web-ui/src/lib/api/missing-precedents.ts | 4 ++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 45669fe..708ad27 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -7915,7 +7915,33 @@ _MP_PROVENANCE_COLS = """, WHERE pic.cited_case_law_id = mp.linked_case_law_id AND COALESCE(src.case_number, '') <> '' ) AS cited_by_precedents, - substring(mp.notes from 'מס''?\\s*([0-9]+)') AS yomon_number""" + substring(mp.notes from 'מס''?\\s*([0-9]+)') AS yomon_number, + -- Bridge to the corpus citation graph (G2: read-time, no stored + -- duplication). For an OPEN gap (no linked_case_law_id yet) find + -- which committee DECISIONS cite this ruling, matched on the + -- normalized docket number (same normalization the relinker uses: + -- strip to digits/dashes, slash->dash). Surfaces "צוטט ע\"י <יו\"ר>" + -- + the deciding case number in the UI. + (SELECT array_agg(DISTINCT src.chair_name ORDER BY src.chair_name) + FROM precedent_internal_citations picc + JOIN case_law src ON src.id = picc.source_case_law_id + AND src.source_kind = 'internal_committee' + WHERE split_part(mp.citation_norm, '|', 2) <> '' + AND regexp_replace(replace(picc.cited_case_number, '/', '-'), + '[^0-9-]', '', 'g') + = split_part(mp.citation_norm, '|', 2) + AND COALESCE(src.chair_name, '') <> '' + ) AS cited_by_chairs, + (SELECT array_agg(DISTINCT src.case_number ORDER BY src.case_number) + FROM precedent_internal_citations picd + JOIN case_law src ON src.id = picd.source_case_law_id + AND src.source_kind = 'internal_committee' + WHERE split_part(mp.citation_norm, '|', 2) <> '' + AND regexp_replace(replace(picd.cited_case_number, '/', '-'), + '[^0-9-]', '', 'g') + = split_part(mp.citation_norm, '|', 2) + AND COALESCE(src.case_number, '') <> '' + ) AS cited_by_decisions""" async def list_missing_precedents( diff --git a/web-ui/src/app/missing-precedents/page.tsx b/web-ui/src/app/missing-precedents/page.tsx index 5188f8b..197b276 100644 --- a/web-ui/src/app/missing-precedents/page.tsx +++ b/web-ui/src/app/missing-precedents/page.tsx @@ -22,12 +22,12 @@ import { MissingPrecedentsTable } from "@/components/missing-precedents/missing- type StatusFilter = MissingPrecedentStatus | "all"; +// Only the two states the chair acts on: open gaps to fill, and closed gaps for +// reference. "הועלה" (transient) and "לא-רלוונטי" were dropped from the filter, +// and "הכל" with them (chair's request, design 09). const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [ { value: "open", label: "פתוח" }, - { value: "uploaded", label: "הועלה" }, { value: "closed", label: "נסגר" }, - { value: "irrelevant", label: "לא-רלוונטי" }, - { value: "all", label: "הכל" }, ]; export default function MissingPrecedentsPage() { @@ -145,15 +145,13 @@ export default function MissingPrecedentsPage() {
מחזור-חיים:{" "} פתוח →{" "} - הועלה →{" "} נסגר. פריט נפתח אוטומטית בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת הפסיקה דרך{" "} linked_case_law_id {" "} - ונסגר. פריט שאינו רלוונטי מסומן{" "} - לא-רלוונטי מבלי שתידרש העלאה. + ונסגר.
diff --git a/web-ui/src/components/missing-precedents/missing-precedents-table.tsx b/web-ui/src/components/missing-precedents/missing-precedents-table.tsx index 0ae0399..eefe116 100644 --- a/web-ui/src/components/missing-precedents/missing-precedents-table.tsx +++ b/web-ui/src/components/missing-precedents/missing-precedents-table.tsx @@ -148,18 +148,43 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) { onClick={() => setOpenId(mp.id)} > -
- {mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")} -
-
- {mp.citation} -
+ {/* Single line when there's no distinct case_name — the old + two-line layout repeated the citation (name fell back to a + truncation of the same citation). Show the name row only + when it adds information. */} + {mp.case_name && mp.case_name.trim() ? ( + <> +
+ {mp.case_name} +
+
+ {mp.citation} +
+ + ) : ( +
+ {mp.citation} +
+ )}
{mp.legal_topic || "—"} - {mp.cited_in_case_number ? ( + {mp.cited_by_decisions?.length ? ( + /* committee decision(s) that cite this missing ruling + (corpus citation-graph bridge) */ + + + {mp.cited_by_decisions[0]} + + {mp.cited_by_decisions.length > 1 ? ( + + +{mp.cited_by_decisions.length - 1} + + ) : null} + + ) : mp.cited_in_case_number ? ( e.stopPropagation()} @@ -173,7 +198,25 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) { )} - {mp.discovery_source === "cited_only" ? ( + {mp.cited_by_chairs?.length ? ( + /* cited by a committee DECISION in the corpus → show the + chair who relied on it (e.g. דפנה תמיר). Bridge from + precedent_internal_citations; takes priority over the + generic discovery-source chips. */ + <> + + {mp.cited_by_chairs[0]} + + {mp.cited_by_chairs.length > 1 ? ( +
+ +{mp.cited_by_chairs.length - 1} יו״ר +
+ ) : null} + + ) : mp.discovery_source === "cited_only" ? ( <> " + "תיק". + cited_by_chairs: string[] | null; + cited_by_decisions: string[] | null; }; export type MissingPrecedentListResponse = {