Files
legal-ai/docs/spec/X1-identifiers.md
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

16 KiB
Raw Blame History

X1 — מודל המזהים הקנוני (Canonical Identifier Model)

קובץ-תחום זה כפוף ל-חוקת המערכת והוא ה-deep-dive על מזהי הישויות של עוזר משפטי. הוא אוכף את G1 (מזהה קנוני מנורמל בכתיבה) ומעמיק את INV-DM2 מ-02-data-model.md. שני הקבצים חייבים להישאר עקביים: 02 מגדיר אילו שדות מזהים כל ישות; X1 מגדיר את הצורה הקנונית של המזהה ואיך הוא מנורמל.

TARGET, לא תיאור-מצב. המודל כאן הוא היעד הקנוני. כל מקום שבו הקוד בפועל (mcp-server/src/legal_mcp/services/db.py) סוטה ממנו — מתועד כ-audit-finding (§4), תסמין, לא התנהגות תקינה. כל טענה על הקוד הקיים מצוטטת file:line ואינה מונחת כתקינה.


1. הצורה הקנונית של case_number

מזהה-התיק (case_number) הוא מספר-תיק מנורמל — לא מחרוזת-ציטוט, לא תווית-תצוגה. הצורה הקנונית מוגדרת ע"י נרמול בנקודת-הכתיבה (write-time canonicalization), כך שכל הרשומות חולקות פורמט יחיד והשוואה היא תמיד שוויון-מחרוזת מול הצורה הקנונית.

הנרמול הקנוני (TARGET — מופעל בכתיבה):

צעד פעולה דוגמה
trim הסרת רווחים מקיפים " 8137/24 ""8137/24"
prefix-strip הסרת קידומת-הליך לפני הספרה הראשונה ("ערר", "בל"מ", "עע"מ") "ערר 8137/24""8137/24"
separator איחוד מפריד /- "8137/24""8137-24"

הצורה הקנונית = המספר הרשמי שהוקצה ע"י הוועדה, נשמר ככתבו — לרבות מקטע-החודש כשהוקצה (למשל 8126-03-25). מספרי-מורשת מסוימים הוקצו ללא חודש (למשל 8126-25); המערכת אסור שתמציא או תוסיף (pad) מקטע-חודש שמעולם לא הוקצה. הנרמול-בכתיבה הוא פורמט-בלבד ודטרמיניסטי (trim · /- · prefix-strip) — הוא אינו מוסיף ואינו מסיר מקטע-חודש. הפורמט המועדף מכאן-ואילך כולל את החודש.

סוג-ההליך (proceeding_type ∈ {ערר, בל"מ}) הוא חלק מהמפתח הקנוני — לא חלק ממחרוזת ה-case_number. הקידומת "ערר"/"בל"מ" מהכותרת נשללת מהמספר ונשמרת בעמודה ייעודית (cases.proceeding_type, db.py:912). כך "ערר 8137/24" ו-"בל"מ 8137/24" הם שתי רשומות מובחנות בעלות אותו case_number=8137-24 ו-proceeding_type שונה.

1א. אורך-הסידורי — אות לסוג-ההליך (נוהל-יו"ר, 2026-06-11)

מבנה ה-case_number הוא <סידורי>-<חודש>-<שנה> (serial-month-year). אורך הסידורי מקודד את סוג-ההליך:

אורך סידורי סוג-הליך דוגמה הערה
4 ספרות ערר 1230-04-26 הליך עיקרי
5 ספרות בל"מ 85074-09-24 בקשה להארכת מועד
  • הספרה הראשונה ממשיכה לקודד את התחום בשני האורכים1→רישוי, 8→היטל השבחה, 9→פיצויים ס'197 (INV-DM/practice_area). תיק בל"מ 85074 → תחום היטל-השבחה.
  • הכלל חד-כיווני: סידורי בן 5 ספרות הוא בל"מ (אות אוטומטי, is_blam_by_number, practice_area.py). סידורי בן 4 ספרות אינו מחייב ערר — בל"מ-מורשת בן 4 ספרות עדיין מזוהה מהנושא (is_blam_subject). הרקע: ירושלים אימצה מספור 5-ספרתי לבל"מ רק עכשיו; ת"א מזה זמן (למשל 81002-01-21).
  • פתיחת תיק חדש מחייבת את צורת serial-month-year המלאה (כולל חודש) — ולידציית-הכתיבה (web-ui/src/lib/schemas/case.ts) דוחה את צורת המורשת ללא-חודש. ההתאמה-הסלחנית-בקריאה (§3) עדיין בולעת רשומות-מורשת בנות שתי-חוליות לצורך חיפוש, לא לצורך יצירה.

נרמול-בכתיבה הוא המנגנון הראשי; התאמה-סלחנית-בקריאה היא נוחות משנית בלבד. כלל-ההנדסה "נרמול לא תיקון-תסמין" (חוקה §6) קובע: מתקנים את הנתון במקור, לא מטליאים בקריאה. אם רשומה נשמרה בצורה לא-קנונית — היעד הוא לנרמל אותה במיגרציה/בכתיבה, לא לסמוך על מנוע-קריאה שיגשר על הפער. ההתאמה-הסלחנית (§3) קיימת כדי לבלוע קלט-משתמש רב-צורני (כותרת Paperclip), לא כדי לתרץ נתון-מאוחסן לא-קנוני.


2. שני מרחבי-מזהים: cases מול case_law

case_number מופיע בשתי טבלאות נפרדות עם שני מרחבי-מזהים שונים וללא FK חוצה-טבלאות ביניהן. בלבול בין השניים הוא כשל-שורש: תיק חי אינו תקדים, ולהפך.

ממד cases (תיק חי) case_law (קורפוס פסיקה)
תפקיד הערר שבטיפול כעת (1xxx/8xxx/9xxx) תקדים — פסיקה חיצונית וגם החלטות-ועדה
מפתח קנוני (case_number, proceeding_type) (case_number, source_kind, proceeding_type) — ראה להלן
אילוץ-ייחודיות uq_cases_number_proc על (case_number, proceeding_type) (db.py:923-924) שני partial unique לפי source_kind (db.py:904-909)
מורשת (הוסרה) case_number TEXT UNIQUE NOT NULL (db.py:76), הוסר V15 (db.py:921-922) case_number TEXT UNIQUE NOT NULL (db.py:368), הוסר V15 (db.py:902-903)
FK חוצה איןcases ו-case_law הם מרחבים נפרדים אין

case_law — מזהה מודע-source_kind. ה-V15 החליפה את UNIQUE(case_number) הגלובלי בשני partial unique indexes (db.py:904-909):

  • internal_committee (החלטות-ועדה פנימיות): UNIQUE(case_number, proceeding_type)uq_case_law_internal_number_proc, WHERE source_kind = 'internal_committee'.
  • חיצוני (external_upload / cited_only / nevo_seed): UNIQUE(case_number)uq_case_law_external_number, WHERE source_kind <> 'internal_committee'.

לכן המזהה הקנוני של case_law הוא הטריפלט (case_number מנורמל, source_kind, proceeding_type) — עקבי עם 02-data-model §2א.

אין הצמדה חוצה-טבלאות. כשהחלטת-תיק מ-cases מצוטטת בהמשך כתקדים, היא נכנסת ל-case_law כרשומה חדשה (source_kind='internal_committee') — לא כ-FK ל-cases. שני המרחבים נשארים עצמאיים; הגישור ביניהם הוא דרך הקליטה (01-ingest.md), לא דרך מפתח-זר.


3. ציטוט מול מזהה — citation_formatted הוא תצוגה, לא מפתח

הציטוט-המלא והמזהה-הקנוני הם שני שדות נפרדים בכוונה:

  • מזהה קנוני = case_number מנורמל (8126-03-25) — המפתח שמשמש לחיפוש, ל-upsert, ולאילוצי-ייחודיות.
  • ציטוט מעוצב = citation_formatted (db.py:1070, V19) — מחרוזת-תצוגה לפי כללי-הציטוט האחיד, למשל: ערר (ועדות ערר - תכנון ובנייה ת"א-יפו) 81002-01-21 **אברהם אגסי נ' הועדה המקומית** (נבו 25.9.2025) (db.py:1067-1068).

הציטוט הוא שדה נגזר לתצוגה — מכיל את המזהה אך גם צדדים, ערכאה, ותאריך-פרסום. הוא לעולם אינו המפתח. אחסון מחרוזת-ציטוט בשדה-המזהה שובר את הנרמול (G1), מערבב תצוגה עם זהות (פוגע ב-1NF — ערך לא-אטומי בשדה-מפתח), ומונע התאמת-שוויון מול המספר המנורמל.


4. Invariants של התחום

INV-ID1: case_number מנורמל בכתיבה — התאמה-סלחנית משנית

כלל: case_number מנורמל לצורה קנונית יחידה בנקודת-הכתיבה בנרמול פורמט-בלבד ודטרמיניסטי (trim · prefix-strip · /-) — הנרמול אינו ממציא ואינו מוסיף מקטע-חודש שלא הוקצה. הצורה הקנונית היא המספר הרשמי שהוקצה (עם חודש כשהוקצה, למשל 8126-03-25), והשוואה-בקריאה היא שוויון מול הצורה הקנונית. התאמה-סלחנית-בקריאה היא נוחות משנית בלבד — היא בולעת קלט-משתמש רב-צורני, ואינה תחליף לנרמול-בכתיבה (G1, כלל-ההנדסה "נרמול לא תיקון-תסמין", חוקה §6). מקורות: SSOT (Single Source of Truth — normalization principle) · E.F. Codd, First Normal Form (CACM 13(6), 1970) · Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017) | סטטוס: verified אכיפה: נרמול-בכתיבה בנקודת-הקליטה (01-ingest.md) + אילוצי-ייחודיות על המפתח הקנוני (uq_cases_number_proc, db.py:923-924; partial unique case_law, db.py:904-909). הפרה ידועה: _normalize_case_number (db.py:1196-1211) מנרמל בקריאה בלבד ("tolerant lookup", db.py:1197), ו-get_case_by_number (db.py:1214-1231) משווה two-pass (case_number=$1 OR replace(btrim(case_number),'/','-')=$2, db.py:1223-1224) — אין מסלול-כתיבה שמקנן את הערך המאוחסן. בנפרד מכך: כשאותו תיק נקלט גם בצורה ללא-חודש וגם עם-חודש (סחף-הזנה, למשל 8126-25 מול 8126-03-25 המתייחסים לתיק אחד), הצורה עם-החודש (הרשמית) היא הקנונית והרשומה החסרה מתואמת אליה — זו בעיית-תיאום (reconciliation), לא חולשה בנרמול (הנרמול אינו אמור לפדד חודש). תיאום רשומות-מורשת מעורבות-צורה הוא פריט ניקיון-נתונים/מיגרציה חד-פעמי (ראה gap-audit / תת-פרויקט 2), לא אלגוריתם-padding בזמן-ריצה → ממצא ל-audit.

INV-ID2: אין ציטוט-מלא כמזהה — הציטוט שדה-תצוגה נגזר

כלל: אף ישות אינה משתמשת במחרוזת-ציטוט-מלאה כמזהה. שדה-המזהה מכיל מספר-תיק מנורמל בלבד; הציטוט-המלא חי בשדה ייעודי נפרד (citation_formatted, db.py:1070) ככלי-תצוגה נגזר (G1, INV-DM2). מקורות: SSOT (Single Source of Truth — normalization principle) · E.F. Codd, First Normal Form (CACM 13(6), 1970) · Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017) | סטטוס: verified אכיפה: הפרדת-שדות ב-schema — מזהה ב-case_number (אילוצי-ייחודיות, db.py:904-909,923-924), ציטוט ב-citation_formatted בלבד (db.py:1070); נרמול-בכתיבה שדוחה מחרוזת-ציטוט בשדה-המזהה. הפרה ידועה: החלטות "סופר" נקלטו עם ציטוט-מלא מאוחסן כ-case_number (שדה-המזהה מכיל את מחרוזת-הציטוט במקום מספר-תיק מנורמל) — חיפוש מול המספר המנורמל נכשל, והפער מתגלגל ל-INV-ID1 (_normalize_case_number רק מטליא בקריאה) → ממצא ל-audit.


5. מצב קיים מול יעד — audit-findings

ההבדלים בין הקוד בפועל ל-TARGET. אלו תסמינים, לא התנהגויות תקינות. כל פריט אומת מול db.py.

  • נרמול בצד-הקריאה בלבד. _normalize_case_number (db.py:1196-1211) מתואר במפורש כ- "tolerant lookup" (db.py:1197) — מסיר קידומת לפני הספרה הראשונה, trim, ו-/- — אך אינו מנרמל את הערך המאוחסן. get_case_by_number (db.py:1214-1231) בונה סביבו two-pass (exact OR normalized, db.py:1223-1224). תסמין: הנרמול חי כתיקון-תסמין בקריאה ולא כקנוניזציה-בכתיבה, בניגוד ל-G1 וכלל-ההנדסה §6. יעד: מסלול-כתיבה שמנרמל את case_number (פורמט-בלבד: trim/prefix-strip//-, ללא המצאת חודש) בנקודת-הקליטה; הקריאה הופכת להשוואת-שוויון פשוטה.
  • רשומות-מורשת מעורבות-צורה (בעיית-תיאום, לא padding). כשאותו תיק נקלט גם כ-8126-25 וגם כ-8126-03-25 (סחף-הזנה), ה-two-pass אינו מזהה אותם כתיק אחד. יעד: תיאום חד-פעמי של הרשומות לצורה הרשמית עם-החודש (הקנונית) במסגרת ניקיון-נתונים/מיגרציה (gap-audit / תת-פרויקט 2) — לא אלגוריתם-padding בזמן-ריצה שממציא חודש.
  • ציטוט-מלא כ-case_number (מורשת). השדה המקורי case_number TEXT UNIQUE NOT NULL (cases db.py:76, case_law db.py:368) לא אכף צורה — מה שאפשר אחסון מחרוזת-ציטוט בשדה זה (החלטות "סופר"). הוחלף ב-partial unique מודע-source_kind ב-V15 (db.py:902-909), אך ללא ולידציית-צורה בכתיבה. יעד: ולידציית-כתיבה שדוחה ערך שאינו מספר-תיק מנורמל ומפנה ציטוט ל-citation_formatted.
  • שני מרחבי-מזהים, סיכון-בלבול בקוד-קריאה. get_case_by_number (db.py:1214) פונה ל-cases בלבד; get_case_law_by_citation (db.py:2503) פונה ל-case_law בלבד — נכון, אך שמות-הפונקציות אינם מבדילים את מרחב-המזהים בבירור. יעד: תיעוד מפורש (קובץ זה) + עקביות שמות שמשקפת cases מול case_law כשני מרחבים נפרדים ללא FK.

6. הפניות-אחיות

  • 00-constitution.mdG1 (מזהה קנוני מנורמל בכתיבה) + כלל-ההנדסה "נרמול לא תיקון-תסמין" (§6).
  • 02-data-model.mdINV-DM2 (מזהה קנוני יחיד) + החוזה הקונקרטי של case_law; X1 הוא ה-deep-dive על אותו מזהה.
  • 01-ingest.md — נקודת-הכתיבה שבה הנרמול-בכתיבה צריך להיאכף.
  • X5-audit-provenance.md — עקיבוּת-מקור (הציטוט כשדה-תצוגה נגזר).