All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
נוהל-יו"ר (2026-06-11): מבנה מספר-תיק = <סידורי>-<חודש>-<שנה>, ואורך הסידורי
מקודד את סוג-ההליך — 4 ספרות = ערר, 5 ספרות = בל"מ. הספרה הראשונה ממשיכה
לקבוע תחום בשני האורכים (1→רישוי, 8→היטל, 9→פיצויים). הכלל חד-כיווני:
5-ספרתי הוא תמיד בל"מ; 4-ספרתי אינו מחייב ערר (בל"מ-מורשת מזוהה מהנושא).
הבאג שדיווח עליו היו"ר: חיפוש פסיקה-חסרה לפי מספר-תיק החזיר 404 על כל ערך
שאינו תיק קיים — שבר את הטבלה תוך כדי הקלדה ועל מספרי 5-ספרות.
תיקונים:
- web/app.py: GET /api/missing-precedents — מסנן case_number שלא תאם תיק מחזיר
רשימה ריקה (200), לא 404. סמנטיקה תקינה ל-collection-filter.
- missing-precedents/page.tsx: debounce (350ms) על שדות-הסינון — קוורי אחד
אחרי שמפסיקים להקליד, לא אחד לכל הקשה.
- practice_area.py: regex סידורי \d{4}→\d{4,5}; case_serial_digits() +
is_blam_by_number() (5⇒בל"מ); derive_subtype_with_blam ו-derive_proceeding_type
מזהים בל"מ גם מ-5-ספרות (בנוסף לנושא). callers: cases.py, internal_decisions.py.
- proofreader.py: דפוסי חילוץ-שם-קובץ \d{3,4}→\d{3,5}.
- web-ui: practice-area.ts (מראָה ל-backend), schemas/case.ts (regex
serial-month-year, 4-or-5 ספרות, superRefine 5⇒בל"מ), placeholder בוויזרד.
- תיעוד: docs/spec/X1-identifiers.md §1א + legal-ai/CLAUDE.md.
Invariants: מקיים G1 (נרמול-במקור — ספרה ראשונה כמקור-אמת יחיד לתחום),
G2 (מסלול-סיווג יחיד, אין כפילות), INV-DM/X1 (מפתח קנוני + proceeding_type).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
7.4 KiB
TypeScript
184 lines
7.4 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { AppShell } from "@/components/app-shell";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
useMissingPrecedents,
|
||
type MissingPrecedentStatus,
|
||
} from "@/lib/api/missing-precedents";
|
||
import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-precedents-table";
|
||
|
||
/**
|
||
* Missing-precedents page (TaskMaster #35).
|
||
*
|
||
* Surfaces citations that party briefs invoke but which aren't yet in the
|
||
* precedent_library. A status filter (chips) narrows the table; each row uses
|
||
* the same table component. Drawer (sheet) opens on row click with metadata +
|
||
* upload form that routes to internal_decision_upload (ערר/בל"מ citations) or
|
||
* precedent_library_upload (court rulings).
|
||
*/
|
||
|
||
type StatusFilter = MissingPrecedentStatus | "all";
|
||
|
||
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() {
|
||
const [caseNumber, setCaseNumber] = useState("");
|
||
const [legalTopic, setLegalTopic] = useState("");
|
||
const [filter, setFilter] = useState<StatusFilter>("open");
|
||
|
||
/* Debounce the filters so the table fires one query after the user stops
|
||
* typing — not one per keystroke. Each intermediate value used to
|
||
* round-trip to the API (and a non-existent case number errored mid-typing). */
|
||
const [caseNumberQ, setCaseNumberQ] = useState("");
|
||
const [legalTopicQ, setLegalTopicQ] = useState("");
|
||
useEffect(() => {
|
||
const t = setTimeout(() => setCaseNumberQ(caseNumber.trim()), 350);
|
||
return () => clearTimeout(t);
|
||
}, [caseNumber]);
|
||
useEffect(() => {
|
||
const t = setTimeout(() => setLegalTopicQ(legalTopic.trim()), 350);
|
||
return () => clearTimeout(t);
|
||
}, [legalTopic]);
|
||
|
||
const counts = useMissingPrecedents({ limit: 1 });
|
||
const byStatus = counts.data?.by_status ?? {};
|
||
|
||
return (
|
||
<AppShell>
|
||
<section className="space-y-6">
|
||
<header className="space-y-3">
|
||
<nav className="text-[0.78rem] text-ink-muted">
|
||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||
<span aria-hidden> · </span>
|
||
<span className="text-navy">פסיקה חסרה בקורפוס</span>
|
||
</nav>
|
||
|
||
{/* title + inline open-count pill (mockup 09 `.open-count`) */}
|
||
<div className="flex items-baseline gap-3.5 flex-wrap">
|
||
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
|
||
{byStatus.open ? (
|
||
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3.5 py-1">
|
||
<span className="text-lg font-bold text-warn tabular-nums leading-none">
|
||
{byStatus.open}
|
||
</span>
|
||
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||
אימות-הלכה ועיגון-מקור (INV-AH). סוכן המחקר רושם פערים אוטומטית;
|
||
היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב אוטומטי בין הקורפוס
|
||
הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
||
</p>
|
||
</header>
|
||
|
||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||
|
||
{/* shared filters */}
|
||
<div className="flex items-end gap-3 flex-wrap">
|
||
<div className="flex-1 min-w-[200px]">
|
||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">תיק (מספר ערר)</label>
|
||
<Input
|
||
value={caseNumber}
|
||
onChange={(e) => setCaseNumber(e.target.value)}
|
||
placeholder="1017-03-26"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
<div className="flex-1 min-w-[200px]">
|
||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">נושא משפטי</label>
|
||
<Input
|
||
value={legalTopic}
|
||
onChange={(e) => setLegalTopic(e.target.value)}
|
||
placeholder="זכות עמידה"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* status filter chips (mockup 09 `.filters`) — active = navy filled */}
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
{STATUS_CHIPS.map((c) => {
|
||
const active = filter === c.value;
|
||
const count =
|
||
c.value === "all"
|
||
? undefined
|
||
: (byStatus[c.value as MissingPrecedentStatus] ?? 0);
|
||
return (
|
||
<button
|
||
key={c.value}
|
||
type="button"
|
||
onClick={() => setFilter(c.value)}
|
||
aria-pressed={active}
|
||
className={`rounded-full border px-4 py-1.5 text-[0.82rem] transition-colors ${
|
||
active
|
||
? "bg-navy text-white border-navy font-semibold"
|
||
: "bg-surface text-ink-soft border-rule font-medium hover:bg-rule-soft/50"
|
||
}`}
|
||
>
|
||
{c.label}
|
||
{count ? (
|
||
<span className="ms-1.5 tabular-nums opacity-80">({count})</span>
|
||
) : null}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<MissingPrecedentsTable
|
||
status={filter === "all" ? "" : filter}
|
||
caseNumber={caseNumberQ || undefined}
|
||
legalTopic={legalTopicQ || undefined}
|
||
/>
|
||
|
||
{/* lifecycle note (mockup 09 `.lifecycle`) */}
|
||
<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>{" "}
|
||
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
||
<LifecycleChip tone="up">הועלה</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">
|
||
linked_case_law_id
|
||
</code>{" "}
|
||
ונסגר. פריט שאינו רלוונטי מסומן{" "}
|
||
<LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
|
||
</div>
|
||
</section>
|
||
</AppShell>
|
||
);
|
||
}
|
||
|
||
type LifecycleTone = "open" | "up" | "closed" | "na";
|
||
|
||
function LifecycleChip({
|
||
tone,
|
||
children,
|
||
}: {
|
||
tone: LifecycleTone;
|
||
children: React.ReactNode;
|
||
}) {
|
||
const cls: Record<LifecycleTone, string> = {
|
||
open: "bg-warn-bg text-warn",
|
||
up: "bg-info-bg text-info",
|
||
closed: "bg-success-bg text-success",
|
||
na: "bg-rule-soft text-ink-muted",
|
||
};
|
||
return (
|
||
<span className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold ${cls[tone]}`}>
|
||
{children}
|
||
</span>
|
||
);
|
||
}
|