Deterministic migration of ~52 internal_committee rows whose case_number holds a full citation → normalized bare number (citation_formatted already correct). DB analysis (2026-05-31): clean 1-token extraction, 0 key-collisions, 0 citation↔case_number mismatches, no month-padding dups. Chair-gated reversible migration (backup→dry-run→approve→apply). One edge for chair: 8047/23 ערר vs בל"מ. External (#68/FU-2c) split out — its citation_formatted is inconsistent. Verified all 11 case_law FKs use id(UUID), not case_number → rename is FK-safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
8.5 KiB
Markdown
102 lines
8.5 KiB
Markdown
# FU-2b — תיאום מזהי `case_number` (Identifier Reconciliation) — עיצוב
|
||
|
||
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-31 · **ענף:** TBD
|
||
**מכסה:** GAP-07, GAP-08 (scope: `internal_committee` בלבד) · **מספק:** INV-ID1, INV-ID2, INV-DM2
|
||
**משימה:** TaskMaster #67 · **תלוי ב:** FU-2a (#60, פונקציית הנרמול) · **סוג:** **data-migration + chair-gate**
|
||
**מחוץ-להיקף:** external_upload → **#68 / FU-2c** (נתונים סותרים, ראה §1).
|
||
|
||
---
|
||
|
||
## 1. הבעיה והיקף (מאומת מול DB, 2026-05-31)
|
||
|
||
`internal_committee` הוא הקורפוס שבו `case_number` חייב להיות **מספר-ועדה מנורמל** (X1 §1), אך
|
||
~52/56 רשומות מחזיקות **ציטוט-מלא** בשדה-המזהה (GAP-08 — "החלטות סופר"), בניגוד ל-INV-ID2
|
||
(ציטוט = שדה-תצוגה נגזר, לעולם לא מזהה).
|
||
|
||
**ממצאי-נתונים שמעצבים את המיגרציה:**
|
||
- **חילוץ דטרמיניסטי ונקי:** כל 56 הרשומות → בדיוק token-מספר אחד (regex `[0-9]{2,6}(?:[-/][0-9]{1,2}){1,2}`). 0 רב-משמעיים, 0 בלתי-פתירים.
|
||
- **עקביות מושלמת:** ב-55/56 המספר המחולץ **מופיע** ב-`citation_formatted`; **0 סתירות**. (1 רשומה בלי citation_formatted — כבר bare.)
|
||
- **0 התנגשויות-מפתח** על (bare, proceeding_type) → **אין dedup**.
|
||
- **אין בעיית with/without-month:** ה"צורות הכפולות" (1024-24 מול 1024-25 וכו') הן **שנים שונות** = תיקים שונים, לא padding.
|
||
- **edge יחיד ליו"ר:** `8047/23` קיים פעמיים — אחת `proceeding_type=ערר`, אחת `בל"מ` (48 chunks כל אחת). לפי X1 אלו **שתי רשומות מובחנות** (ערר מול בל"מ), אך זהות chunk-count מצדיקה אימות-יו"ר שאינן כפילות מתויגת-שגוי.
|
||
|
||
**external מופרד (#68):** ב-external נמצאה **סתירה** (`case_number=25226-04-25` מול
|
||
`citation_formatted=1975/24`) — ה-citation_formatted נוצר בנפרד ואינו ground-truth אמין; דורש
|
||
טיפול נפרד. בנוסף, זהות פסיקה-חיצונית היא טבעית הציטוט (אין מספר-ועדה). מחוץ ל-FU-2b.
|
||
|
||
## 2. ההכרעה (מבוססת X1 + ממצאי-נתונים)
|
||
|
||
הצורה הקנונית של `case_number` ל-internal = **trim · prefix-strip · `/`→`-`** על המספר הרשמי,
|
||
**בלי להמציא/להסיר חודש** (X1 §1; מקורות: Codd 1NF · Kleppmann DDIA · SSOT — verified ב-X1).
|
||
המיגרציה **דטרמיניסטית** (לא LLM): מחלצת את ה-token המספרי היחיד ומנרמלת. הציטוט כבר חי
|
||
ב-`citation_formatted` — אין מה לנגוע בו.
|
||
|
||
**דפוס-בטיחות (chair-gated reversible migration):** גיבוי-לפני-שינוי → dry-run שמפיק טבלת-תיאום
|
||
→ **שער-אישור-יו"ר** → apply מפורש → אימות. זהו דפוס סטנדרטי למיגרציה בלתי-הפיכה על נתוני-ייצור.
|
||
|
||
## 3. הרכיבים
|
||
|
||
- **סקריפט** `scripts/fu2b_reconcile_internal_case_numbers.py` (לא MCP tool — מיגרציה חד-פעמית מבוקרת):
|
||
- `--dry-run` (ברירת-מחדל): מפיק טבלת-תיאום `data/audit/fu2b-reconciliation-<ts>.csv` +
|
||
`.md` קריא ליו"ר. עמודות: `id, current_case_number, proposed_bare, proceeding_type,
|
||
citation_formatted, consistency_ok, flag`.
|
||
- `--apply`: דורש קובץ-אישור (ראה §4); מגבה ואז מבצע.
|
||
- מעבד **רק** `source_kind='internal_committee'` ו**רק** רשומות שבהן `proposed_bare != case_number`
|
||
(idempotent — already-bare לא נוגעים).
|
||
- **חילוץ:** `_extract_bare(case_number) -> str|None` — regex token יחיד + `_canonical_case_number`
|
||
(מ-FU-2a, db.py) לנרמול הסופי. אם 0 או >1 tokens → `None` + flag `NEEDS_CHAIR`.
|
||
- **consistency guard:** אם `proposed_bare` **לא** מופיע ב-`citation_formatted` → flag `MISMATCH` (לא
|
||
יוחל אוטומטית; ליו"ר). (כיום 0 כאלה, אך הסקריפט בודק בזמן-ריצה.)
|
||
- **גיבוי:** לפני apply, כתיבת `data/audit/fu2b-backup-<ts>.csv` = `(id, old_case_number)` לכל רשומה
|
||
שתשונה → revert-script טריוויאלי.
|
||
- **edge 8047/23:** הסקריפט **לא** ממזג; מסמן את הזוג ב-flag `DUP_CHECK` בטבלה. ההכרעה (מובחנות מול
|
||
כפילות) היא של היו"ר; אם כפילות — מחיקה ידנית נפרדת (לא חלק מה-apply הדטרמיניסטי).
|
||
|
||
## 4. שער-אישור-היו"ר (chair gate)
|
||
|
||
1. הרצת `--dry-run` → טבלת-תיאום (`.md`) + סיכום (כמה ישתנו, אילו flags).
|
||
2. **הצגה לדפנה**: הטבלה (52 שורות: ציטוט-נוכחי → bare מוצע) + ה-edge של 8047/23. היא מסמנת
|
||
שורות שגויות (אם יש) ומכריעה על 8047/23.
|
||
3. תיקון flags לפי הערותיה (אם יש), ואז `--apply --approved data/audit/fu2b-approved-<ts>.csv`
|
||
(קובץ-האישור = הטבלה לאחר סקירתה; הסקריפט מחיל רק שורות שאושרו).
|
||
4. אימות אחרי apply: כל internal `case_number` תואם regex bare; 0 ציטוטים בשדה-המזהה;
|
||
`search`/`get_case_by_number` עדיין פותרים (FU-2a tolerant-read + הנרמול).
|
||
|
||
## 5. אינטראקציה עם FU-2a (forward-consistency)
|
||
|
||
FU-2a `_canonical_case_number` מנרמל prefix+separator אך **אינו מחלץ מספר מתוך ציטוט-מלא**. לכן
|
||
אם קליטה עתידית תעביר ציטוט-מלא כ-`case_number`, ייווצר שוב מזהה מלוכלך. **הערכת-סיכון:** נמוכה —
|
||
טופס-ההעלאה וה-MCP tool מעבירים שדה-`case_number` נפרד (בד"כ נקי). **החלטה:** FU-2b הוא ניקוי-נתונים
|
||
בלבד; הקשחת-כתיבה (חילוץ-token גם ב-create) **לא בהיקף** — תיפתח רק אם יתגלה caller שמעביר ציטוט.
|
||
(מתועד; לא לשנות התנהגות-כתיבה בלי ראיה.)
|
||
|
||
## 6. שינויי-התנהגות וסיכון
|
||
|
||
| שינוי | השפעה | סיכון |
|
||
|--------|--------|--------|
|
||
| `case_number` של ~52 internal → bare | חיפוש exact-match על המספר עובד; (case_number,proceeding_type) נקי | נמוך — דטרמיניסטי, גיבוי, שער-יו"ר, 0 collisions |
|
||
| 8047/23 edge | אולי מחיקת רשומה כפולה | בינוני — **רק** בהחלטת-יו"ר, מחיקה ידנית נפרדת, לא ב-apply האוטומטי |
|
||
| citation_formatted | **לא משתנה** (כבר תקין) | אין |
|
||
| FK/relations | `case_law_relations`/`precedent_internal_citations` מפנים ל-`id` (UUID), לא ל-case_number | אין — שינוי case_number לא שובר קשרים |
|
||
| chunks/embeddings | מפתח-זר `case_law_id` (UUID) — לא תלוי ב-case_number | אין — re-index לא נדרש |
|
||
|
||
## 7. אסטרטגיית בדיקה
|
||
|
||
- **בדיקות-יחידה offline** (`tests/test_fu2b_reconcile.py`): `_extract_bare` — token יחיד→bare מנורמל;
|
||
ציטוט מלא→המספר הנכון (דוגמאות אמיתיות: `"ערר (...) 403/17 אהרון ברק..."`→`403-17`,
|
||
`"...8136-10-24 שחר..."`→`8136-10-24` חודש נשמר); 0/רב-token→None+flag; consistency guard.
|
||
- **dry-run מול DB מקומי**: הטבלה מופקת, מספר-השורות-לשינוי = ~52, 0 MISMATCH, 1 DUP_CHECK (8047).
|
||
- **apply בסביבת-בדיקה**: על עותק/תיק-בדיקה — אימות idempotency (הרצה שנייה = 0 שינויים) + revert מהגיבוי.
|
||
- ה-apply בייצור רץ **רק אחרי אישור-יו"ר** (לא חלק מה-CI/PR; ידני ומבוקר).
|
||
|
||
## 8. סדר-ביצוע
|
||
|
||
1. בדיקות אדומות ל-`_extract_bare` + consistency guard.
|
||
2. `_extract_bare` + הסקריפט (`--dry-run` בלבד תחילה) + הפקת טבלת-תיאום + גיבוי.
|
||
3. בדיקות ירוקות + dry-run מול DB → הפקת הטבלה.
|
||
4. **עצירה: הצגת הטבלה + 8047/23 ליו"ר (דפנה)** — שער-אישור.
|
||
5. (אחרי אישור) מימוש `--apply --approved` + אימות + revert-script.
|
||
6. הרצת apply בייצור (מבוקר) + אימות-אחרי + TaskMaster #67.
|
||
|
||
> צעדים 1–3 לא דורשים את דפנה (אני מכין הכל). צעד 4 הוא שער-האישור. צעדים 5–6 אחרי אישורה.
|