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>
142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|