Files
legal-ai/docs/spec/X1-identifiers.md
2026-05-30 16:41:37 +00:00

157 lines
13 KiB
Markdown
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.
# X1 — מודל המזהים הקנוני (Canonical Identifier Model)
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **מזהי הישויות**
של עוזר משפטי. הוא אוכף את [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) (מזהה
קנוני מנורמל בכתיבה) ומעמיק את [INV-DM2](02-data-model.md#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות)
מ-[02-data-model.md](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"` |
| padding | צורת-מספר אחידה (סדרתי-שנה, ומחוז כשקיים) | `"8126-25"``"8126-03-25"` |
> סוג-ההליך (`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א](02-data-model.md#2א-case_law--החוזה-הקונקרטי).
**אין הצמדה חוצה-טבלאות.** כשהחלטת-תיק מ-`cases` מצוטטת בהמשך כתקדים, היא נכנסת ל-`case_law`
כרשומה *חדשה* (`source_kind='internal_committee'`) — לא כ-FK ל-`cases`. שני המרחבים נשארים
עצמאיים; הגישור ביניהם הוא דרך הקליטה ([01-ingest.md](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](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)),
מערבב תצוגה עם זהות (פוגע ב-1NF — ערך לא-אטומי בשדה-מפתח), ומונע התאמת-שוויון מול המספר
המנורמל.
---
## 4. Invariants של התחום
### INV-ID1: `case_number` מנורמל בכתיבה — התאמה-סלחנית משנית
**כלל:** `case_number` מנורמל לצורה קנונית יחידה **בנקודת-הכתיבה** (trim · prefix-strip ·
`/``-` · padding), והשוואה-בקריאה היא שוויון מול הצורה הקנונית. **התאמה-סלחנית-בקריאה היא
נוחות משנית בלבד** — היא בולעת קלט-משתמש רב-צורני, ואינה תחליף לנרמול-בכתיבה ([G1](00-constitution.md#inv-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](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` (padding לא מכוסה בנרמול-הקריאה)
→ ממצא ל-[audit](../audit-report.md).
### INV-ID2: אין ציטוט-מלא כמזהה — הציטוט שדה-תצוגה נגזר
**כלל:** אף ישות **אינה** משתמשת במחרוזת-ציטוט-מלאה כמזהה. שדה-המזהה מכיל מספר-תיק מנורמל
בלבד; הציטוט-המלא חי בשדה ייעודי נפרד (`citation_formatted`, `db.py:1070`) ככלי-תצוגה נגזר
([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה), [INV-DM2](02-data-model.md#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](../audit-report.md).
---
## 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](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) וכלל-ההנדסה
§6. **יעד:** מסלול-כתיבה שמנרמל את `case_number` (כולל padding) בנקודת-הקליטה; הקריאה הופכת
להשוואת-שוויון פשוטה.
- **padding לא מכוסה אפילו בנרמול-הקריאה.** הנרמול הקיים מטפל ב-prefix/separator/trim בלבד,
לא ב-padding מחוז/שנה — ולכן `8126-25``8126-03-25` נכשל גם ב-two-pass. **יעד:** padding
כחלק מהצורה הקנונית (§1), מנורמל בכתיבה לכל הרשומות.
- **ציטוט-מלא כ-`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.md](00-constitution.md) — [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)
(מזהה קנוני מנורמל בכתיבה) + כלל-ההנדסה "נרמול לא תיקון-תסמין" (§6).
- [02-data-model.md](02-data-model.md) — [INV-DM2](02-data-model.md#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות)
(מזהה קנוני יחיד) + החוזה הקונקרטי של `case_law`; X1 הוא ה-deep-dive על אותו מזהה.
- [01-ingest.md](01-ingest.md) — נקודת-הכתיבה שבה הנרמול-בכתיבה צריך להיאכף.
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור (הציטוט כשדה-תצוגה נגזר).