# 02 — מודל-הנתונים (Data Model & Completeness Contract) קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **מודל-הנתונים הקנוני (TARGET)** של עוזר משפטי — הישויות, שדות-המפתח, והיכן יושב כל פריט מואנדקס. הוא אוכף את [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) (מזהה קנוני יחיד), [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (חוזה-שלמות) ו- [G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) (re-index בשינוי-תוכן). > **TARGET, לא תיאור-מצב.** המודל כאן הוא היעד הקנוני. כל מקום שבו ה-schema בפועל > (`mcp-server/src/legal_mcp/services/db.py`) סוטה ממנו — מתועד כ-**audit-finding** (§4), > תסמין לאיחוד, לא התנהגות תקינה. כל טענה על ה-schema הקיים מצוטטת `file:line`. --- ## 1. הישויות הקנוניות הטבלה מונה את ישויות-הליבה. "מזהה-קנוני" = השדה היחיד המזהה רשומה ([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)). | ישות | תפקיד | מזהה-קנוני | שדות-מפתח (מאומתים `db.py`) | |------|--------|-------------|------------------------------| | `cases` | תיק ערר חי (1xxx/8xxx/9xxx) | `case_number` + `proceeding_type` | `title`, `status`, `practice_area`, `appeal_subtype`, `proceeding_type`, `chair_name` (`db.py:74-91,182-189,747,912`) | | `documents` | מסמך-מקור משויך לתיק | `id` (UUID); FK→`cases` | `doc_type`, `title`, `file_path`, `extracted_text`, `extraction_status`, `page_count` (`db.py:93-104`) | | `document_chunks` | chunk של מסמך-תיק + embedding | `id`; FK→`documents`/`cases` | `chunk_index`, `content`, `section_type`, `embedding vector(1024)`, `page_number` (`db.py:106-116`) | | `case_law` | קורפוס פסיקה — חיצוני **וגם** החלטות-ועדה | ראה [§2 + INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות) | `case_name`, `court`, `practice_area`, `source_kind`, `proceeding_type`, `source_type`, `headnote`, `summary`, `subject_tags`, `extraction_status`, `halacha_extraction_status` (`db.py:366-378,522-526,599-611,883,907`) | | `precedent_chunks` | chunk של פסק-דין מואנדקס (`source_kind='external_upload'`/`internal_committee`) | `id`; FK→`case_law` | `chunk_index`, `content`, `section_type`, `page_number`, `embedding vector(1024)`, `content_tsv` (`db.py:624-634,776`) | | `halachot` | הלכה מחולצת — כלל + ציטוט מילולי | `id`; FK→`case_law` | `rule_statement`, `supporting_quote`, `rule_type`, `practice_areas`, `subject_tags`, `confidence`, `quote_verified`, `review_status`, `embedding`, `rule_tsv` (`db.py:644-666,780`) | | `decisions` | החלטת-תיק מנוסחת (גרסה) | `id`; `UNIQUE(case_id, version)` | `version`, `status`, `outcome`, `outcome_summary` (`db.py:299-314`) | | `decision_blocks` | בלוק (12) של החלטה | `id`; `UNIQUE(decision_id, block_id)` | `block_id`, `block_index`, `content`, `status` (`db.py:317-334`) | | `claims` | טענת-צד (בלוק ז) | `id`; FK→`cases` | `party_role`, `claim_text`, `source_document`, `claim_type`, `claim_handling` (`db.py:349-359,506-512`) | | `chair_feedback` | הערת-יו"ר על טיוטה | `id`; FK→`cases` | `block_id`, `feedback_text`, `category`, `lesson_extracted`, `resolved` (`db.py:452-462`) | | `missing_precedents` | תקדים חסר שהתבקש ולא נמצא | `id` | (`db.py:806`) — backlog ל-quality-at-source | | `style_corpus` | קורפוס-סגנון של דפנה (אימון) | `id`; FK→`documents` | `decision_number`, `full_text`, `practice_area`, `appeal_subtype` (`db.py:118-131`) | > שכבות-עזר נוספות (`document_image_embeddings`, `precedent_image_embeddings` — multimodal, > `db.py:707,726`; `case_law_relations` — שרשרת-תיק, `db.py:754`; `precedent_internal_citations` > — גרף-ציטוטים, `db.py:937`) הן נגזרות ([G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)): > משוחזרות מהמקור, לא מקור-אמת עצמאי. --- ## 2. חוזה-שלמות לכל ישות (Completeness Contract) [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) דורש: **רשומה אינה "שמישה / ניתנת-לחיפוש" עד ששדות-החובה שלה מולאו ואומתו מול spec מפורש.** כל ישות מגדירה שתי רמות — **usable** (קיימת ומזוהה) ו-**searchable** (חשופה לאחזור). רשומה שנכשלת בחוזה **מסומנת ומדווחת — לא מתקבלת בשקט** (חוקה §6, "אין בליעה שקטה"). ### 2א. `case_law` — החוזה הקונקרטי המזהה הקנוני אינו `case_number` לבדו: `case_law` נושאת **שני** unique partial indexes לפי `source_kind` (`db.py:904-909`) — חיצוני: `UNIQUE(case_number)`; פנימי: `UNIQUE(case_number, proceeding_type)`. לכן המזהה הקנוני הוא **(`case_number` מנורמל, `source_kind`, `proceeding_type`)**. **רמת usable** (רשומה לגיטימית): - `case_number` קנוני מנורמל-בכתיבה ([INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות) — **לא** ציטוט-מלא) - `case_name` לא-ריק (לא fallback לציטוט/למספר) - `court` לא-ריק - `practice_area ∈ {rishuy_uvniya, betterment_levy, compensation_197}` (אכוף ב-CHECK, `db.py:614-617`) - `source_kind` מהמילון (`external_upload` / `cited_only` / `internal_committee` / `nevo_seed`) (`db.py:599-601`, `internal_decisions.py:4`) - `proceeding_type ∈ {ערר, בל"מ}` כשפנימי (אכוף ב-CHECK, `db.py:897-899`) **רמת searchable** (חשוף לאחזור — מעבר ל-usable): - **≥1 `precedent_chunk`** עם `embedding` לא-NULL (אחרת אין מה לאחזר סמנטית) - **metadata לא-ריק:** לפחות אחד מ-`headnote` / `summary` / `subject_tags` מלא — אלו השדות ש-search מציג ומסנן לפיהם - `extraction_status = completed` (מטא-דאטה הושלם, `db.py:603`) **אכיפה מפורשת:** רשומה שעוברת usable אך נכשלת ב-searchable — **מסומנת `searchable=false` ולא מוחזרת מ-search**, ומופיעה ב-health-check כ-backlog. היא **אינה מתקבלת בשקט** כ"זמינה". ### 2ב. חוזה תמציתי לישויות נוספות - `documents` → usable: `file_path`+`doc_type`; searchable: `extraction_status=completed` ו-`extracted_text` לא-ריק ו-≥1 `document_chunk` עם embedding. - `halachot` → usable: `rule_statement`+`supporting_quote`; **searchable: `review_status ∈ {approved, published}` בלבד** — `pending_review`/`rejected` מוסתרות מ-`search_precedent_library` (שער-הלכה ידני, `db.py:644-660`, [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)). - `decision_blocks` → usable: `block_id`∈12-הבלוקים; "מוכן": `status=final` ו-`content` לא-ריק. - `chair_feedback` → usable: `feedback_text`+`category` מהמילון; "פתוח" עד `resolved=true`. ### 2ג. ישויות-נגזרות (אחסון-ניתוחים) מעבר לישויות-המקור, המערכת **שומרת ניתוחים נגזרים** — תוצרי-חילוץ של LLM/קוד. אלו כפופים לכללי ה-provenance של [X8](X8-field-provenance.md) ולשערי [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant): | ישות-נגזרת | מקור-מילוי | שער-אישור | קישור-מקור | |------------|------------|-----------|------------| | `claims` | OPUS (`extract_claims`) | — | `source_document` (string, לא-FK) | | `legal_arguments` (+`legal_argument_propositions`) | OPUS (`aggregate_claims_to_arguments`) | **חסר** (בניגוד ל-halachot) | `cited_precedents TEXT[]` (לא-FK) | | `appraiser_facts` | OPUS (`extract_appraiser_facts`) | — | `document_id` (FK); `appraiser_side` default `''` | | `halachot` | OPUS (`halacha_extractor`) | **`review_status`** ✓ | `case_law_id` (FK); `quote_verified` | | `decision_blocks` / `decision_paragraphs` | Opus/script (`write_block`) | `status` | `model_used` + audit-event provenance (FU-7); `citations JSONB` ללא-FK | --- ## 3. Invariants של התחום ### INV-DM1: searchable רק כשחוזה-השלמות מתקיים **כלל:** רשומת `case_law` נחשבת **searchable** אך ורק כשחוזה-השלמות של [§2א](#2א-case_law--החוזה-הקונקרטי) מתקיים במלואו (מזהה קנוני · `case_name`/`court`/`practice_area`/`source_kind` · ≥1 chunk עם embedding · metadata לא-ריק). רשומה שנכשלת **מסומנת `searchable=false` ומדווחת ל-health-check — לא מוחזרת מ-search ולא מתקבלת בשקט**. **מקורות:** ISO 8000 (completeness) · DAMA-UK *Six Primary Dimensions for Data Quality* (2013, completeness) · ISO 15489-1:2016 (records reliability/usability) | סטטוס: verified **אכיפה:** ולידציית-כתיבה בנקודת-הקליטה ([01-ingest.md](01-ingest.md) צעד 8) + בדיקת-בריאות תקופתית שמסמנת backlog; הסינון נאכף בשכבת-החיפוש ([03-retrieval.md](03-retrieval.md)). אוכף את [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש). **הפרה ידועה:** ערן סופר 8046/24 אונדקס כ-searchable עם `headnote`/`summary`/`subject_tags` ריקים — המסלול הפנימי לא תיזמן חילוץ-מטא-דאטה ([01-ingest INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג), `internal_decisions.py:208`) → ממצא ל-[audit](../audit-report.md). ### INV-DM2: מזהה קנוני יחיד לכל ישות **כלל:** לכל ישות **מזהה קנוני אחד**, מנורמל בכתיבה. **אסור** ששדה-המזהה יאחסן ציטוט-מלא — `case_number` הוא מספר-תיק מנורמל (`8126-03-25`), **לא** מחרוזת-ציטוט (`ערר 8126/24 פלוני נ' הוועדה (נבו...)`). הציטוט המלא חי בשדה ייעודי נפרד (`citation_formatted`, `db.py:1070`), לא במזהה. **מקורות:** SSOT (Single Source of Truth — normalization) · E.F. Codd, First Normal Form (CACM 13(6), 1970) · Martin Kleppmann, *Designing Data-Intensive Applications* (O'Reilly, 2017) | סטטוס: verified **אכיפה:** unique partial indexes על המזהה הקנוני (`db.py:904-909`) + נרמול-בכתיבה ([X1-identifiers.md](X1-identifiers.md)); ציטוט-מלא ב-`citation_formatted` בלבד. אוכף את [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה). **הפרה ידועה:** החלטות "סופר" נקלטו עם **ציטוט-מלא כ-`case_number`** (שדה-המזהה של רשומה מכיל את מחרוזת-הציטוט במקום מספר-תיק מנורמל) — חיפוש מול `8126-03-25` נכשל, ו-`_normalize_case_number` (`db.py:1196-1211`) רק **מטליא בקריאה** (סלחני, לא קנוני), בניגוד ל-[G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) → ממצא ל-[audit](../audit-report.md). ### INV-DM3: שינוי-תוכן ⇒ re-index **כלל:** כל שינוי בתוכן-המקור של ישות מואנדקסת (`content` של chunk, `rule_statement`/`supporting_quote` של הלכה, `full_text`/`extracted_text` של מסמך) מפעיל **re-index** של ה-embedding **ושל ה-tsvector** הנגזרים. אין embedding או `content_tsv`/`rule_tsv`/`meta_tsv` מיושנים מול התוכן. **מקורות:** Pinecone (index freshness / data sync) · Weaviate (re-vectorization on update) · RAG freshness (Lewis et al., 2020, NeurIPS) | סטטוס: verified **אכיפה:** טריגר re-embed בנקודת-העדכון + בדיקת-בריאות לגילוי drift; ה-tsvectors `GENERATED ALWAYS … STORED` (`db.py:776-788,1083-1090`) מתעדכנים אוטומטית, אך ה-`embedding` **אינו** generated — הוא תלוי-טריגר. מפורט ב-[03-retrieval.md](03-retrieval.md). אוכף את [G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן). **הפרה ידועה:** — ### INV-DM4: לכל ישות-נגזרת — provenance מוצהר **כלל:** כל ישות-נגזרת (claims, legal_arguments, appraiser_facts, decision_blocks, halachot) נושאת **provenance** — מי/מה הפיק (מודל, גרסה, זמן) ולאילו chunks/מקורות היא קשורה. מופע של [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai); מקביל ל-[X8 INV-FP1](X8-field-provenance.md). **מקורות:** ISO 8000-110 (data lineage) · DAMA-DMBOK2 (lineage) · ISO 15489-1:2016 (records authenticity) | סטטוס: verified **אכיפה:** עמודות-provenance + קישור block→source (חלקית דרך audit-event ב-FU-7/GAP-19; ל-legal_arguments טרם). **הפרה ידועה:** `legal_arguments` ללא provenance; `embedding` ללא model/version ([gap-audit GAP-42](gap-audit.md)). ### INV-DM5: פלט-ניתוח של LLM נכנס בשער-אישור (כמו halachot) **כלל:** ישות-נגזרת שמוּלאת ע"י LLM ומשפיעה על ההחלטה נכנסת **לא-מאושרת** עד אישור-יו"ר — אותו שער כמו `halachot.review_status`. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant); תואם [X8 INV-FP3](X8-field-provenance.md). **מקור-סמכות:** דפוס `halachot.review_status` (`db.py:659`); [05-qa-review.md](05-qa-review.md). (פרויקטלי-תפעולי — משרת G10.) **אכיפה:** שדה-סטטוס-אישור על ישויות-נגזרות מהותיות. **הפרה ידועה:** `legal_arguments` **חסר** שער-אישור — נכתב ומשמש ללא בקרת-יו"ר ([gap-audit GAP-39](gap-audit.md)). ### INV-DM6: ולידציה — CHECK-enums, FK לציטוטים, ללא טבלאות-מקבילות **כלל:** ערכי-enum נאכפים ב-CHECK (לא TEXT חופשי); ציטוט-מקור נשמר כ-FK (לא string/array חופשי); אין שתי טבלאות לאותה ישות. מופע של [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים). **הנדסי.** **מקורות:** E.F. Codd (referential integrity, CACM 1970) · ISO 8000 (validity) · Kleppmann *DDIA* | סטטוס: verified **אכיפה:** CHECK על enums; FK על `cited_precedents`/`decision_paragraphs.citations`; איחוד `case_precedents`↔`case_law`. **הפרה ידועה:** 20+ enums כ-TEXT חופשי; `legal_arguments.cited_precedents TEXT[]` ללא-FK (הזיות-LLM נבלעות); `case_precedents` מול `case_law` מקבילות ([gap-audit GAP-40/42/43](gap-audit.md)). --- ## 4. מצב קיים מול יעד — audit-findings ההבדלים בין ה-schema בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול `db.py`. - **`case_law` כפולת-תפקיד ללא מזהה מודע-סוג בכתיבה.** טבלה אחת משרתת פסיקה חיצונית **וגם** החלטות-ועדה, מובדלות ב-`source_kind` (`db.py:599`). המזהה הקנוני האמיתי הוא טריפלט (`case_number, source_kind, proceeding_type`, `db.py:904-909`), אך השדה `case_number TEXT UNIQUE NOT NULL` המקורי (`db.py:368`) הוסר רק ב-V15 (`db.py:902-903`) — מורשת שאפשרה את הפרת [INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות). **יעד:** נרמול-בכתיבה אכוף + ציטוט-מלא רק ב-`citation_formatted`. - **`summary` קיים על `case_law` אך לא בחוזה-הקליטה הפנימי.** העמודה קיימת (`db.py:373`) אך המסלול הפנימי אינו ממלא אותה (כפועל-יוצא מהיעדר חילוץ-מטא-דאטה, [INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)). **יעד:** searchable מותנה ב-metadata לא-ריק ([INV-DM1](#inv-dm1-searchable-רק-כשחוזה-השלמות-מתקיים)). - **שני שדות-סטטוס-חילוץ נפרדים, ללא דגל-`searchable` מפורש.** `extraction_status` + `halacha_extraction_status` (`db.py:603-605`) מתארים תהליך, אך אין שדה יחיד שמסמן "עבר חוזה-שלמות → searchable". **יעד:** דגל/view נגזר ש-search מסנן לפיו, מגובה health-check. - **`embedding` אינו `GENERATED` (בניגוד ל-tsvector).** ה-tsvectors מסונכרנים אוטומטית (`db.py:776,780,1083`), אך ה-`embedding vector(1024)` תלוי-טריגר חיצוני — נקודת-drift אפשרית ל-[INV-DM3](#inv-dm3-שינוי-תוכן--re-index). **יעד:** טריגר re-embed מובטח + health-check ל-drift. - **`halachot.review_status` כשער-searchable ללא נראות-backlog.** הסינון תקין (`pending_review` מוסתר, `db.py:659`), אך אין נראות כמה ממתינות — תואם את ההפרה הידועה ב-[G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (10/19 מאושרות, התגלה במקרה). **יעד:** health-check חושף backlog-הלכות. --- ## 5. הפניות-אחיות - [00-constitution.md](00-constitution.md) — invariants גלובליים (G1, G4, G6) + כללי-הנדסה. - [01-ingest.md](01-ingest.md) — חוזה-הקליטה שמייצר את הרשומות; חוזה-השלמות כאן אוכף את תוצריו. - [03-retrieval.md](03-retrieval.md) — שכבת-האחזור שאוכפת את הסינון searchable + re-index. - [X1-identifiers.md](X1-identifiers.md) — נרמול המזהה הקנוני בכתיבה (בסיס ל-INV-DM2). - [X5-audit-provenance.md](X5-audit-provenance.md) — שלמות-רשומה + עקיבוּת-מקור. - [X8-field-provenance.md](X8-field-provenance.md) — מקור-מילוי השדות (בסיס ל-INV-DM4/DM5). - [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — הכלים שמייצרים את הישויות-הנגזרות.