feat(missing-precedents): bridge cited-by-chair + decision number; open/closed tabs; single-line citation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -7915,7 +7915,33 @@ _MP_PROVENANCE_COLS = """,
|
|||||||
WHERE pic.cited_case_law_id = mp.linked_case_law_id
|
WHERE pic.cited_case_law_id = mp.linked_case_law_id
|
||||||
AND COALESCE(src.case_number, '') <> ''
|
AND COALESCE(src.case_number, '') <> ''
|
||||||
) AS cited_by_precedents,
|
) 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(
|
async def list_missing_precedents(
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-
|
|||||||
|
|
||||||
type StatusFilter = MissingPrecedentStatus | "all";
|
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 }[] = [
|
const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
|
||||||
{ value: "open", label: "פתוח" },
|
{ value: "open", label: "פתוח" },
|
||||||
{ value: "uploaded", label: "הועלה" },
|
|
||||||
{ value: "closed", label: "נסגר" },
|
{ value: "closed", label: "נסגר" },
|
||||||
{ value: "irrelevant", label: "לא-רלוונטי" },
|
|
||||||
{ value: "all", label: "הכל" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function MissingPrecedentsPage() {
|
export default function MissingPrecedentsPage() {
|
||||||
@@ -145,15 +145,13 @@ export default function MissingPrecedentsPage() {
|
|||||||
<div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
|
<div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
|
||||||
<b className="text-ink-soft">מחזור-חיים:</b>{" "}
|
<b className="text-ink-soft">מחזור-חיים:</b>{" "}
|
||||||
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
||||||
<LifecycleChip tone="up">הועלה</LifecycleChip> →{" "}
|
|
||||||
<LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
|
<LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
|
||||||
בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
|
בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
|
||||||
הפסיקה דרך{" "}
|
הפסיקה דרך{" "}
|
||||||
<code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
|
<code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
|
||||||
linked_case_law_id
|
linked_case_law_id
|
||||||
</code>{" "}
|
</code>{" "}
|
||||||
ונסגר. פריט שאינו רלוונטי מסומן{" "}
|
ונסגר.
|
||||||
<LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -148,18 +148,43 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) {
|
|||||||
onClick={() => setOpenId(mp.id)}
|
onClick={() => setOpenId(mp.id)}
|
||||||
>
|
>
|
||||||
<TableCell className="max-w-[440px]">
|
<TableCell className="max-w-[440px]">
|
||||||
|
{/* 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() ? (
|
||||||
|
<>
|
||||||
<div className="text-sm text-navy font-semibold truncate">
|
<div className="text-sm text-navy font-semibold truncate">
|
||||||
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
{mp.case_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||||||
{mp.citation}
|
{mp.citation}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-navy font-semibold truncate" dir="rtl">
|
||||||
|
{mp.citation}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-sm text-ink">{mp.legal_topic || "—"}</span>
|
<span className="text-sm text-ink">{mp.legal_topic || "—"}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{mp.cited_in_case_number ? (
|
{mp.cited_by_decisions?.length ? (
|
||||||
|
/* committee decision(s) that cite this missing ruling
|
||||||
|
(corpus citation-graph bridge) */
|
||||||
|
<span className="inline-flex items-baseline gap-1.5">
|
||||||
|
<span className="text-sm text-navy font-semibold tabular-nums" dir="ltr">
|
||||||
|
{mp.cited_by_decisions[0]}
|
||||||
|
</span>
|
||||||
|
{mp.cited_by_decisions.length > 1 ? (
|
||||||
|
<span className="text-[0.7rem] text-ink-muted">
|
||||||
|
+{mp.cited_by_decisions.length - 1}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : mp.cited_in_case_number ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/cases/${encodeURIComponent(mp.cited_in_case_number)}`}
|
href={`/cases/${encodeURIComponent(mp.cited_in_case_number)}`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -173,7 +198,25 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-ink">
|
<TableCell className="text-sm text-ink">
|
||||||
{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. */
|
||||||
|
<>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full whitespace-nowrap bg-success-bg text-success border-transparent"
|
||||||
|
>
|
||||||
|
{mp.cited_by_chairs[0]}
|
||||||
|
</Badge>
|
||||||
|
{mp.cited_by_chairs.length > 1 ? (
|
||||||
|
<div className="text-[0.7rem] text-ink-muted mt-1">
|
||||||
|
+{mp.cited_by_chairs.length - 1} יו״ר
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : mp.discovery_source === "cited_only" ? (
|
||||||
<>
|
<>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ export type MissingPrecedent = {
|
|||||||
discovery_source: string | null; // manual | cited_only | digest | court_fetch
|
discovery_source: string | null; // manual | cited_only | digest | court_fetch
|
||||||
cited_by_precedents: string[] | null; // corpus precedents citing a cited_only stub
|
cited_by_precedents: string[] | null; // corpus precedents citing a cited_only stub
|
||||||
yomon_number: string | null; // digest gap's source yomon number
|
yomon_number: string | null; // digest gap's source yomon number
|
||||||
|
// Bridge to the corpus citation graph: committee decisions (and their chairs)
|
||||||
|
// that cite this still-missing ruling — fills "צוטט ע״י <יו״ר>" + "תיק".
|
||||||
|
cited_by_chairs: string[] | null;
|
||||||
|
cited_by_decisions: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MissingPrecedentListResponse = {
|
export type MissingPrecedentListResponse = {
|
||||||
|
|||||||
Reference in New Issue
Block a user