Files
legal-ai/web-ui/src/lib/practice-area.ts
Chaim e8bcb9c1ea
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
fix(cases): מספור 5-ספרתי לבל"מ — סיווג, ולידציה, וחיפוש פסיקה-חסרה
נוהל-יו"ר (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>
2026-06-12 06:16:42 +00:00

142 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Client-side mirror of mcp-server/src/legal_mcp/services/practice_area.py.
*
* Keep the enum values and derivation logic in sync with the backend — the
* server is the authority, but the UI needs the labels and derivation for
* UX (auto-fill, badges, filters). If the server adds a new practice_area
* or subtype, extend the arrays below.
*
* See also: legal-ai/docs/practice-area-separation.md
*/
export type PracticeArea =
| "appeals_committee"
| "national_insurance"
| "labor_law";
export type AppealSubtype =
| "building_permit"
| "betterment_levy"
| "compensation_197"
/* בל"מ — בקשה להארכת מועד להגשת ערר. שלושה מסלולים נפרדים. */
| "extension_request_building_permit"
| "extension_request_betterment_levy"
| "extension_request_compensation"
| "unknown";
export const PRACTICE_AREAS: ReadonlyArray<{
value: PracticeArea;
label: string;
enabled: boolean;
}> = [
{ value: "appeals_committee", label: "ועדת ערר", enabled: true },
{ value: "national_insurance", label: "ביטוח לאומי", enabled: false },
{ value: "labor_law", label: "דיני עבודה", enabled: false },
];
export const APPEAL_SUBTYPES: ReadonlyArray<{
value: AppealSubtype;
label: string;
}> = [
{ value: "building_permit", label: "רישוי ובנייה" },
{ value: "betterment_levy", label: "היטל השבחה" },
{ value: "compensation_197", label: "פיצויים (ס' 197)" },
/* The "בל\"מ" prefix is dropped from these labels because the
* proceeding_type field now drives the בל"מ badge. Keeping the domain
* label here lets a row show "רישוי ובנייה" with a separate בל"מ
* pill instead of double-marking it. */
{ value: "extension_request_building_permit", label: "רישוי ובנייה" },
{ value: "extension_request_betterment_levy", label: "היטל השבחה" },
{ value: "extension_request_compensation", label: "פיצויים (ס' 197)" },
{ value: "unknown", label: "לא ידוע" },
];
/* בל"מ subtypes — תת-קבוצה. שימוש: badges, filters */
export const BLAM_SUBTYPES: ReadonlySet<AppealSubtype> = new Set([
"extension_request_building_permit",
"extension_request_betterment_levy",
"extension_request_compensation",
]);
export function isBlamSubtype(s?: AppealSubtype | null): boolean {
return s != null && BLAM_SUBTYPES.has(s);
}
export const PRACTICE_AREA_LABELS: Record<PracticeArea, string> =
Object.fromEntries(PRACTICE_AREAS.map((p) => [p.value, p.label])) as Record<
PracticeArea,
string
>;
export const APPEAL_SUBTYPE_LABELS: Record<AppealSubtype, string> =
Object.fromEntries(APPEAL_SUBTYPES.map((s) => [s.value, s.label])) as Record<
AppealSubtype,
string
>;
/*
* Derive the appeal_subtype from a case number. Mirrors the Python
* `derive_subtype` in practice_area.py. The convention is the case-number
* first digit: 1xxx → building_permit, 8xxx → betterment_levy,
* 9xxx → compensation_197. Everything else, including non-appeals_committee
* domains, returns 'unknown'.
*/
export function deriveSubtype(
caseNumber: string,
practiceArea: PracticeArea = "appeals_committee",
): AppealSubtype {
if (practiceArea !== "appeals_committee") return "unknown";
const first = caseNumber.trim().match(/^(\d)/)?.[1];
if (first === "1") return "building_permit";
if (first === "8") return "betterment_levy";
if (first === "9") return "compensation_197";
return "unknown";
}
/*
* Detect a בל"מ subject (בקשה להארכת מועד). Mirrors the Python
* `is_blam_subject` in practice_area.py. Accepts common variants.
*/
const BLAM_SUBJECT_RE = /(?:בקשה\s+להארכת\s+מועד|בל["״]מ|הארכת\s+מועד\s+להגשת)/i;
export function isBlamSubject(subject: string): boolean {
return Boolean(subject) && BLAM_SUBJECT_RE.test(subject);
}
/*
* Digit-count of the case serial (the leading numeric group, before
* month/year): "8126-03-25" → 4, "81002-01-21" → 5. Returns null when no
* serial is present. Mirrors `case_serial_digits()` in practice_area.py.
*/
export function caseSerialDigits(caseNumber: string): number | null {
const m = caseNumber.trim().match(/(\d{4,5})/);
return m ? m[1].length : null;
}
/*
* True iff the case serial has 5 digits — the post-reform convention for
* בל"מ (4 digits = ערר). One-directional: 5 ⇒ בל"מ, but 4 does NOT imply
* ערר (a legacy 4-digit בל"מ is caught via the subject). Mirrors
* `is_blam_by_number()` in practice_area.py.
*/
export function isBlamByNumber(caseNumber: string): boolean {
return caseSerialDigits(caseNumber) === 5;
}
/*
* Like deriveSubtype() but also detects בל"מ from the subject OR a 5-digit
* serial. Mirrors `derive_subtype_with_blam()` in practice_area.py.
*/
export function deriveSubtypeWithBlam(
caseNumber: string,
subject: string = "",
practiceArea: PracticeArea = "appeals_committee",
): AppealSubtype {
const base = deriveSubtype(caseNumber, practiceArea);
if (!isBlamSubject(subject) && !isBlamByNumber(caseNumber)) return base;
if (base === "building_permit") return "extension_request_building_permit";
if (base === "betterment_levy") return "extension_request_betterment_levy";
if (base === "compensation_197") return "extension_request_compensation";
return base;
}