Files
legal-ai/docs/spec/X1-identifiers.md

15 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 שונה.

נרמול-בכתיבה הוא המנגנון הראשי; התאמה-סלחנית-בקריאה היא נוחות משנית בלבד. כלל-ההנדסה "נרמול לא תיקון-תסמין" (חוקה §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 — עקיבוּת-מקור (הציטוט כשדה-תצוגה נגזר).