The extractor classified rule_type by SOURCE bindingness (higher-court→binding, committee→persuasive) instead of by rule KIND. The gold-set proved it: 'binding' appeared on 19/19 external rulings & 0 committees; 'persuasive' on 13/13 committees & 0 external — only 58% agreement with the human role tags. The two axes (authority vs rule role) were crammed into one enum. This splits them per INV-DM7: - authority (binding/persuasive) — DERIVED from case_law.precedent_level (עליון/מנהלי→binding, ועדת_ערר_מחוזית→persuasive), never stored, never LLM-guessed. New helper halacha_quality.derive_authority; surfaced read-only in list_halachot / goldset_list / search results. - rule_type — now the rule ROLE only: holding/interpretive/procedural/ application/obiter. Both extractor prompts unified to this vocabulary; _coerce_halacha no longer defaults rule_type from the source; legacy binding→holding / persuasive→interpretive fold for safety. UI: authority shown as a separate read-only badge (gold=מחייב / muted=משכנע) across the review queue, precedent detail, and gold-set; the gold-set role selector drops binding/persuasive and adds מהותי (holding). Migration: scripts/halacha_rule_role_backfill.py re-classifies the 276 pre-split binding/persuasive rows into a genuine role via local claude_session (run after deploy). Gold-set correct_type/ai_correct_type 'binding'→'holding' via SQL. Sources (≥3, per research-decision policy): OASIS LegalRuleML v1.0 (appliesAuthority/Strength as metadata orthogonal to rule logic) · SemEval-2023 Task 6 LegalEval (rhetorical roles by function, authority kept separate) · Bluebook signals (weight-of-authority is a separate dimension). Invariants: ESTABLISHES INV-DM7. Upholds G1 (normalize at source — extractor classifies role, system derives authority) and G2 (single source of truth — authority derived, not a parallel stored field). Tests: 211 pass + new derive_authority/coerce coverage. web-ui build + tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
21 KiB
02 — מודל-הנתונים (Data Model & Completeness Contract)
קובץ-תחום זה כפוף ל-חוקת המערכת ומגדיר את מודל-הנתונים הקנוני (TARGET) של עוזר משפטי — הישויות, שדות-המפתח, והיכן יושב כל פריט מואנדקס. הוא אוכף את G1 (מזהה קנוני יחיד), G4 (חוזה-שלמות) ו- G6 (re-index בשינוי-תוכן).
TARGET, לא תיאור-מצב. המודל כאן הוא היעד הקנוני. כל מקום שבו ה-schema בפועל (
mcp-server/src/legal_mcp/services/db.py) סוטה ממנו — מתועד כ-audit-finding (§4), תסמין לאיחוד, לא התנהגות תקינה. כל טענה על ה-schema הקיים מצוטטתfile:line.
1. הישויות הקנוניות
הטבלה מונה את ישויות-הליבה. "מזהה-קנוני" = השדה היחיד המזהה רשומה (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 | 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): משוחזרות מהמקור, לא מקור-אמת עצמאי.
2. חוזה-שלמות לכל ישות (Completeness Contract)
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 — לא ציטוט-מלא)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לא-ריק ו-≥1document_chunkעם embedding.halachot→ usable:rule_statement+supporting_quote; searchable:review_status ∈ {approved, published}בלבד —pending_review/rejectedמוסתרות מ-search_precedent_library(שער-הלכה ידני,db.py:644-660, G10).decision_blocks→ usable:block_id∈12-הבלוקים; "מוכן":status=finalו-contentלא-ריק.chair_feedback→ usable:feedback_text+categoryמהמילון; "פתוח" עדresolved=true.
2ג. ישויות-נגזרות (אחסון-ניתוחים)
מעבר לישויות-המקור, המערכת שומרת ניתוחים נגזרים — תוצרי-חילוץ של LLM/קוד. אלו כפופים לכללי ה-provenance של X8 ולשערי G10:
| ישות-נגזרת | מקור-מילוי | שער-אישור | קישור-מקור |
|---|---|---|---|
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א
מתקיים במלואו (מזהה קנוני · 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 צעד 8) + בדיקת-בריאות
תקופתית שמסמנת backlog; הסינון נאכף בשכבת-החיפוש (03-retrieval.md). אוכף את
G4.
הפרה ידועה: ערן סופר 8046/24 אונדקס כ-searchable עם headnote/summary/subject_tags
ריקים — המסלול הפנימי לא תיזמן חילוץ-מטא-דאטה (01-ingest INV-ING3,
internal_decisions.py:208) → ממצא ל-audit.
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); ציטוט-מלא ב-citation_formatted בלבד. אוכף את
G1.
הפרה ידועה: החלטות "סופר" נקלטו עם ציטוט-מלא כ-case_number (שדה-המזהה של רשומה מכיל את
מחרוזת-הציטוט במקום מספר-תיק מנורמל) — חיפוש מול 8126-03-25 נכשל, ו-_normalize_case_number
(db.py:1196-1211) רק מטליא בקריאה (סלחני, לא קנוני), בניגוד ל-G1
→ ממצא ל-audit.
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. אוכף את
G6.
הפרה ידועה: —
INV-DM4: לכל ישות-נגזרת — provenance מוצהר
כלל: כל ישות-נגזרת (claims, legal_arguments, appraiser_facts, decision_blocks, halachot) נושאת
provenance — מי/מה הפיק (מודל, גרסה, זמן) ולאילו chunks/מקורות היא קשורה. מופע של
G9; מקביל ל-X8 INV-FP1.
מקורות: 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).
INV-DM5: פלט-ניתוח של LLM נכנס בשער-אישור (כמו halachot)
כלל: ישות-נגזרת שמוּלאת ע"י LLM ומשפיעה על ההחלטה נכנסת לא-מאושרת עד אישור-יו"ר — אותו שער כמו
halachot.review_status. מופע של G10; תואם X8 INV-FP3.
מקור-סמכות: דפוס halachot.review_status (db.py:659); 05-qa-review.md. (פרויקטלי-תפעולי — משרת G10.)
אכיפה: שדה-סטטוס-אישור על ישויות-נגזרות מהותיות.
הפרה ידועה: legal_arguments חסר שער-אישור — נכתב ומשמש ללא בקרת-יו"ר (gap-audit GAP-39).
INV-DM6: ולידציה — CHECK-enums, FK לציטוטים, ללא טבלאות-מקבילות
כלל: ערכי-enum נאכפים ב-CHECK (לא TEXT חופשי); ציטוט-מקור נשמר כ-FK (לא string/array חופשי); אין שתי
טבלאות לאותה ישות. מופע של G4 ו-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).
INV-DM7: סיווג-הלכה — סמכות (נגזרת) ⊥ תפקיד-כלל (מסווג). שני צירים, לא enum אחד
כלל: ל-halachot שני צירי-סיווג אורתוגונליים שאסור לערבב בשדה אחד:
- סמכות (
authority) — נגזרת בלבד, לא מאוחסנת, לא מנוחשת ע"י LLM.binding(מקור מחייב את הוועדה: עליון/מנהלי) מולpersuasive(מקור משכנע: ועדת-ערר אחרת). נגזרת דטרמיניסטית מ-case_law.precedent_level(עליון/מנהלי→binding;ועדת_ערר_מחוזית→persuasive). מקור-אמת יחיד — מחושבת בקריאה, אין עמודה כפולה (G1/G2). - תפקיד-כלל (
rule_type/rule_role) — מסווג ע"י ה-LLM.holding(עיקרון מהותי הכרחי להכרעה — ratio/Wambaugh) ·interpretive(פרשנות חוק/מונח/תכנית) ·procedural(סדר-דין: סמכות/מועדים/נטל) ·application(החלה תלוית-עובדות — לרוב לא-הלכה) ·obiter(אמרת-אגב).binding/persuasiveאינם ערכי תפקיד — הם סמכות-מקור. הנדסי. מופע של G1 (נרמול במקור: המחלץ מסווג תפקיד, לא ממציא סמכות נגזירה) ו-G2. מקורות: OASIS LegalRuleML v1.0 (appliesAuthority/Strengthכ-metadata אורתוגונלי, נפרד מלוגיקת-הכלל) · SemEval-2023 Task 6 LegalEval (rhetorical-roles לפי תפקיד, סמכות נשמרת בנפרד) · Bluebook signals (משקל-סמכות = ציר נפרד מהפרופוזיציה) | סטטוס: verified (≥3 מקורות). ההפרה שתוקנה:halacha_extractorסיווגrule_typeלפי bindingness-של-המקור (_coerce_halacha(is_binding), ברירת-מחדלbinding/persuasive, guard binding→persuasive) — כלומר חישב סמכות במסווה של תפקיד. אומת אמפירית על מדגם-הזהב:bindingשימש 19/19 פסקים חיצוניים ו-0 ועדות;persuasive13/13 ועדות ו-0 חיצוניים → סיווג-לפי-מקור, התאמה לתיוג-אנושי 58% בלבד. התיקון מעביר סמכות לציר-נגזר ומשחרר את ה-LLM לסווג תפקיד נטו.
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. יעד: נרמול-בכתיבה אכוף + ציטוט-מלא רק ב-citation_formatted.summaryקיים עלcase_lawאך לא בחוזה-הקליטה הפנימי. העמודה קיימת (db.py:373) אך המסלול הפנימי אינו ממלא אותה (כפועל-יוצא מהיעדר חילוץ-מטא-דאטה, INV-ING3). יעד: searchable מותנה ב-metadata לא-ריק (INV-DM1).- שני שדות-סטטוס-חילוץ נפרדים, ללא דגל-
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. יעד: טריגר re-embed מובטח + health-check ל-drift.halachot.review_statusכשער-searchable ללא נראות-backlog. הסינון תקין (pending_reviewמוסתר,db.py:659), אך אין נראות כמה ממתינות — תואם את ההפרה הידועה ב-G10 (10/19 מאושרות, התגלה במקרה). יעד: health-check חושף backlog-הלכות.
5. הפניות-אחיות
- 00-constitution.md — invariants גלובליים (G1, G4, G6) + כללי-הנדסה.
- 01-ingest.md — חוזה-הקליטה שמייצר את הרשומות; חוזה-השלמות כאן אוכף את תוצריו.
- 03-retrieval.md — שכבת-האחזור שאוכפת את הסינון searchable + re-index.
- X1-identifiers.md — נרמול המזהה הקנוני בכתיבה (בסיס ל-INV-DM2).
- X5-audit-provenance.md — שלמות-רשומה + עקיבוּת-מקור.
- X8-field-provenance.md — מקור-מילוי השדות (בסיס ל-INV-DM4/DM5).
- X9-mcp-tool-contract.md — הכלים שמייצרים את הישויות-הנגזרות.