15 KiB
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 (exactORnormalized,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(casesdb.py:76,case_lawdb.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.md — G1 (מזהה קנוני מנורמל בכתיבה) + כלל-ההנדסה "נרמול לא תיקון-תסמין" (§6).
- 02-data-model.md — INV-DM2
(מזהה קנוני יחיד) + החוזה הקונקרטי של
case_law; X1 הוא ה-deep-dive על אותו מזהה. - 01-ingest.md — נקודת-הכתיבה שבה הנרמול-בכתיבה צריך להיאכף.
- X5-audit-provenance.md — עקיבוּת-מקור (הציטוט כשדה-תצוגה נגזר).