feat(corpus): Stage A — corpus tagging fixes + prevention layer
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m8s

מתקן את הבאג של תיוג שגוי לועדות ערר ומונע חזרתו:

**Code changes:**
* New MCP tool `internal_decision_upload` (chair_name+district required)
  — sole supported path for ingesting committee decisions; tags
  source_kind='internal_committee' automatically.
* Citation guard in `precedent_library_upload` rejects citations starting
  with "ערר" or "בל\"מ" with a directive to use internal_decision_upload.
* `practice_area.py` taxonomy unification: PRACTICE_AREAS now accepts
  both multi-tenant (appeals_committee/national_insurance/labor_law)
  and domain (rishuy_uvniya/betterment_levy/compensation_197) values.
  New helper `to_db_practice_area(multi_tenant, subtype) -> domain`.

**Agent docs:**
* legal-researcher (+5K): upload-tool decision flowchart, code samples
  per source_kind, district enum (ירושלים/מרכז/תל אביב/צפון/דרום/חיפה/ארצי)
* legal-ceo, legal-analyst, legal-writer, legal-qa, HEARTBEAT — taxonomy
  awareness + source_kind-aware citation patterns + research_complete
  as valid status.
* Fixed two pre-existing wrong practice_area values in examples
  (histael_hashbacha→betterment_levy, pitsuim_197→compensation_197).

Closes TaskMaster #30(parts), #38(parts), #39 (root cause).
DB-side backfill + CHECK constraints applied directly via psql:
* 11 cases.practice_area corrected (1xxx→rishuy, 8xxx→betterment)
* 6 case_law records reclassified external_upload→internal_committee
  with inferred district
* 6 chair_name backfilled from full_text (5 שרית אריאלי + 1 דפנה תמיר)
* 88 new halachot extracted for newly-uploaded precedents
  (אנטרים + ירושלים שקופה 1112/22 + אגא וכט)
* CHECK constraints: cases.practice_area enum, case_law internal⇒district

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 07:40:18 +00:00
parent 8153bc9f03
commit c6e368e4f7
10 changed files with 469 additions and 14 deletions

View File

@@ -181,6 +181,36 @@ python3 /home/chaim/legal-ai/scripts/notify.py \
--- ---
## §7. סטטוסי תיק תקפים (case status flow)
הסטטוסים שאתה עשוי לראות ב-`case.status` (לפי `legal-ceo.md` "מפת סטטוסים"):
```
new → proofread → documents_ready → analyst_verified → research_complete*
→ outcome_set → direction_approved → analysis_enriched → ready_for_writing
→ drafted → qa_passed / qa_failed → exported
```
`research_complete` — **valid status** (לא legacy מחוסר תוקף). מנותב ע"י `legal-researcher.md` שלב 5 כשמחקר תקדימים רץ בנפרד מהמנתח (תרחיש מתקדם). ה-CEO יודע לטפל בו כאילו זה `analyst_verified` (ראה `legal-ceo.md` "מפת סטטוסים").
---
## §8. ניתוב upload פסיקה לקורפוס — flowchart מהיר
```
חיים העלה PDF פסיקה לתיק → ה-citation הוא:
├── "ערר NNNN/YY" או "בל"מ NNNN/YY"
│ → internal_decision_upload (חובה chair_name + district)
└── "עע"מ / בר"מ / עמ"נ / בג"ץ / ע"א / ע"פ / רע"א / רע"פ / ת"א / ת"מ"
→ precedent_library_upload (external_upload)
```
- **`internal_decision_upload`** דורש: `file_path`, `case_number`, `chair_name`, `district`. district מתוך הרשימה: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.
- **`precedent_library_upload`** לא מקבל chair_name/district. אם תנסה להעלות "ערר ..." דרכו — citation guard ידחה.
- פירוט מלא: `legal-researcher.md` סעיף "איזה כלי upload להשתמש".
---
## נתיבי API — הפניה ל-skill הרשמי ## נתיבי API — הפניה ל-skill הרשמי
| פעולה | איפה ב-skill | | פעולה | איפה ב-skill |

View File

@@ -63,6 +63,26 @@ tools:
- חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית - חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית
- ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין) - ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין)
## טקסונומיה — שני namespaces ל-`practice_area`
⚠️ **חובה לדעת לפני שאתה כותב practice_area לכל כלי MCP או יוצר תיק חדש.**
יש שני namespaces שונים:
| Axis | ערכים | איפה משתמשים |
|------|--------|--------------|
| **A. Multi-tenant (legacy/routing)** | `appeals_committee`, `national_insurance`, `labor_law` | בחירת tenant. הסוכנים בוועדת ערר תמיד `appeals_committee` |
| **B. Domain (DB + filters)** | `rishuy_uvniya`, `betterment_levy`, `compensation_197` | **DB columns + כל פילטר ב-`search_precedent_library` / `search_internal_decisions`** |
**כלל זהב — בכל קריאה לכלי שמחפש או כותב לקורפוס, השתמש ב-Axis B בלבד:**
- 1xxx → `rishuy_uvniya`
- 8xxx → `betterment_levy`
- 9xxx → `compensation_197`
**יצירת תיק חדש (`case_create`):** ב-DB, העמודה `cases.practice_area` מאוכפת ע"י CHECK constraint לערכי Axis B (או ריק). **אסור** לכתוב `appeals_committee` ל-`cases.practice_area` — זה ידחה. אם אתה לא בטוח באיזה axis תיק קיים נמצא, קרא קודם `case_get` ובדוק.
**זיהוי בל"מ (בקשה להארכת מועד):** אם ה-subject של מסמך/תיק מכיל "בקשה להארכת מועד" או הקידומת "בל\"מ" — זהו סיווג ייחודי (במיוחד תיקי 8xxx). חלץ זאת בעת הניתוח וציין ב-`appeal_subtype` כאחד הסיווגים המקובלים. בל"מ הוא דיוני בעיקרו ולכן הניתוח שלו שונה — לרוב יש טענת סף יחידה (האם להאריך) ולא דיון מהותי. סמן זאת בפלט כדי שהכותב ידע לבחור תבנית קצרה.
## הבחנה קריטית — 3 סוגי פריטים מחולצים ## הבחנה קריטית — 3 סוגי פריטים מחולצים
| סוג (claim_type) | מה זה | מי אמר | | סוג (claim_type) | מה זה | מי אמר |
@@ -181,8 +201,8 @@ tools:
| סיווג תיק | practice_area | | סיווג תיק | practice_area |
|------------|---------------| |------------|---------------|
| 1xxx (רישוי ובניה) | `rishuy_uvniya` | | 1xxx (רישוי ובניה) | `rishuy_uvniya` |
| 8xxx (היטל השבחה) | `histael_hashbacha` | | 8xxx (היטל השבחה) | `betterment_levy` |
| 9xxx (פיצויים ס' 197) | `pitsuim_197` | | 9xxx (פיצויים ס' 197) | `compensation_197` |
אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת. אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת.

View File

@@ -18,6 +18,8 @@ tools:
- mcp__legal-ai__list_chair_feedback - mcp__legal-ai__list_chair_feedback
- mcp__legal-ai__search_case_documents - mcp__legal-ai__search_case_documents
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
- mcp__legal-ai__search_internal_decisions
- mcp__legal-ai__internal_decision_upload
- mcp__legal-ai__workflow_status - mcp__legal-ai__workflow_status
- mcp__legal-ai__processing_status - mcp__legal-ai__processing_status
- mcp__legal-ai__get_metrics - mcp__legal-ai__get_metrics
@@ -78,6 +80,48 @@ tools:
| `docs/daphna-procedural-patterns.md` | תבניות פרוצדורליות (החלטת ביניים, חזרה לשמאי) | CEO + writer (8xxx בלבד) | | `docs/daphna-procedural-patterns.md` | תבניות פרוצדורליות (החלטת ביניים, חזרה לשמאי) | CEO + writer (8xxx בלבד) |
| `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) | | `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) |
## טקסונומיה — שני namespaces ל-`practice_area` (חובה לדעת)
⚠️ **קריטי לפני שאתה כותב practice_area לכל כלי MCP — יש שני namespaces שונים שמוגדרים במערכת:**
| Axis | ערכים | איפה משתמשים |
|------|--------|--------------|
| **A. Multi-tenant (legacy, routing)** | `appeals_committee`, `national_insurance`, `labor_law` | רק לבחירת ה-tenant ברמת המוצר. הסוכנים בוועדת ערר תמיד `appeals_committee` |
| **B. Domain (DB columns + filters)** | `rishuy_uvniya`, `betterment_levy`, `compensation_197` | **כל קריאה ל-`search_precedent_library` / `search_internal_decisions` / `precedent_library_upload` / `internal_decision_upload`** — זה ה-namespace הקובע |
**המרה אוטומטית:** `to_db_practice_area(multi_tenant_pa, appeal_subtype)` ממירה Axis A → Axis B (משתמש פנימי בלבד).
**כללי ברזל לכלי MCP:**
- בכל קריאה לכלי שמחפש או כותב לקורפוס פסיקה — **השתמש בערכי Axis B בלבד**:
- 1xxx (רישוי ובניה) → `rishuy_uvniya`
- 8xxx (היטל השבחה) → `betterment_levy`
- 9xxx (פיצויים ס' 197) → `compensation_197`
- **אסור** לעבור `appeals_committee` כ-`practice_area` ל-`search_precedent_library` — זה ייתן 0 תוצאות (הקורפוס מאוחסן ב-Axis B).
- DB constraint `cases_practice_area_check` אוכף: practice_area של תיק חייב להיות אחד מהשלושה ב-Axis B (או ריק).
## כלי MCP חדשים (יוני 2026) — חובה לקרוא
### `internal_decision_upload` — העלאת החלטת ועדת ערר לקורפוס
החלטות של ועדות ערר אחרות (`source_kind='internal_committee'`) עוברות **רק** דרך כלי זה — לא דרך `precedent_library_upload` (citation guard דוחה).
**חתימה (חובה כל ארבעת השדות):**
```
internal_decision_upload(
file_path=..., # נתיב מלא ל-PDF/DOCX/RTF/TXT/MD
case_number=..., # "ערר 1024-25" / "בל\"מ 8126/25" / וכו'
chair_name=..., # שם יו"ר — חובה (לחיפוש סלקטיבי)
district=..., # ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי
... # case_name, court, decision_date, practice_area, וכו' — אופציונליים
)
```
**מי משתמש בפועל:** ב-`legal-researcher` (ראה `legal-researcher.md`). ה-CEO רק יודע שזה קיים — אם חוקר מדווח שלא הצליח להעלות החלטת ועדת ערר, ה-CEO בודק שה-chair_name + district סופקו.
### `search_internal_decisions` — חיפוש בהחלטות ועדות ערר
`search_decisions` = רק החלטות דפנה (style corpus). `search_internal_decisions` = כל ועדות הערר בכל המחוזות, עם פילטרים `chair_name` ו-`district`. ה-CEO משתמש בכלי זה בתרחישי routing מתקדמים — בד"כ ה-researcher ו-analyst הם המשתמשים העיקריים.
## הסוכנים שלך ## הסוכנים שלך
| סוכן | Agent ID | תפקיד | | סוכן | Agent ID | תפקיד |
@@ -597,7 +641,7 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) | | `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` | | `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
| `analyst_verified` | CEO (אחרי שלב A) | → שלב B (סיכום + שאלת תוצאה לחיים). המנתח כבר ביצע את המחקר כחלק מהניתוח — אין ליצור issue לחוקר. | | `analyst_verified` | CEO (אחרי שלב A) | → שלב B (סיכום + שאלת תוצאה לחיים). המנתח כבר ביצע את המחקר כחלק מהניתוח — אין ליצור issue לחוקר. |
| `research_complete` | (מנתח — legacy, או תרחיש מיוחד עם חוקר) | → שלב B (סיכום + שאלת תוצאה לחיים). בזרימה הרגילה המנתח לא מגדיר סטטוס זה — רק `documents_ready`. אם תראה סטטוס זה, בדוק אם `analysis-and-research.md` קיים לפני §B. | | `research_complete` | מנתח / חוקר תקדימים (valid status — legacy + תרחישים מתקדמים) | → שלב B (סיכום + שאלת תוצאה לחיים). **זה סטטוס תקף**, לא שגיאה. בזרימה הרגילה המנתח מגדיר `documents_ready`, אבל אם החוקר רץ בנפרד (`legal-researcher.md` שלב 5) הוא מעדכן ל-`research_complete`. אם תראה סטטוס זה, בדוק שגם `analysis-and-research.md` וגם `precedent-research.md` קיימים, ואז המשך ל-§B כרגיל. |
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C | | `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
| `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה | | `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה |
| `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) | | `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) |

View File

@@ -15,7 +15,9 @@ tools:
- mcp__legal-ai__workflow_status - mcp__legal-ai__workflow_status
- mcp__legal-ai__search_case_documents - mcp__legal-ai__search_case_documents
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
- mcp__legal-ai__search_internal_decisions
- mcp__legal-ai__precedent_library_get - mcp__legal-ai__precedent_library_get
- mcp__legal-ai__precedent_list
- mcp__legal-ai__halacha_review - mcp__legal-ai__halacha_review
--- ---
@@ -145,6 +147,39 @@ tools:
- האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)? - האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)?
- **ציטוטי פסיקה חיצונית בבלוק י** — לכל ציטוט (`citation` + `supporting_quote`) שמופיע, חפש ב-`search_precedent_library` (subject_tag הרלוונטי) וודא שהציטוט קיים בקורפוס ושהלכה אושרה. ציטוט שלא תואם להלכה מאושרת = critical. - **ציטוטי פסיקה חיצונית בבלוק י** — לכל ציטוט (`citation` + `supporting_quote`) שמופיע, חפש ב-`search_precedent_library` (subject_tag הרלוונטי) וודא שהציטוט קיים בקורפוס ושהלכה אושרה. ציטוט שלא תואם להלכה מאושרת = critical.
### 9. צירוף פסיקה ל-DB (`precedent_attach`) — critical
לכל ציטוט פסיקה בבלוק י (חיצוני או internal_committee), **חייב להיות רישום ב-`case_precedents`** דרך `precedent_attach` של ה-researcher.
**שיטת בדיקה:**
1. הרץ `precedent_list(case_number)` — קבל רשימת כל הציטוטים שנרשמו ל-DB.
2. סרוק את בלוק י (וטענות סף) וזהה כל ציטוט פסיקה (citation + quote).
3. **לכל ציטוט**: ודא שהוא מופיע ב-`precedent_list`. אם חסר → `qa = fail` (critical, חוסם ייצוא). דווח אילו ציטוטים לא נרשמו.
**למה זה חשוב:** ה-DOCX exporter ו-Hermes curator קוראים מ-`case_precedents`. ציטוט שנמצא רק בטקסט ולא ב-DB יחמיץ at-export-time validation וניתוח Hermes.
### 10. מראה מקום מלא בציטוטים — warning
לכל ציטוט פסיקה בבלוק י, ודא שהוא כולל:
- **מספר תיק מלא** (לא רק "פלוני נ' פלמוני")
- **ערכאה** (עליון / מנהלי / מחוזי / שלום / ועדת ערר)
- **תאריך / `פורסם בנבו`** או `פורסם ב-`
- **`page_reference`** כשמדובר בציטוט ארוך מתוך פס"ד
אם חסר אחד מהשלושה הראשונים → **`qa = warning`**, דווח לחיים בcomment + הצע למלא. (לא חוסם — לא כל פסק דין יש לו פאג'ינציה.)
### 11. תקפות סטטוס תיק (status_validity) — sanity check
בדוק `case_get(case_number).status` — הוא צריך להיות בערכים תקפים. הזרימה הכוללת:
```
new → proofread → documents_ready → analyst_verified → research_complete (legacy/optional)
→ outcome_set → direction_approved → analysis_enriched → ready_for_writing
→ drafted (אתה כאן!) → qa_passed / qa_failed → exported
```
⚠️ **`research_complete` הוא valid status** (לא bug, לא legacy ערומה). ב-`legal-researcher.md` שלב 5 הוא הסטטוס שהחוקר מגדיר בסיום מחקר. אם תיק במצב זה נשלח אליך לפני `drafted` — דווח, אל תכשיל.
#### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה) #### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה)
- האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה? - האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה?
- האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה? - האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה?
@@ -165,6 +200,9 @@ tools:
| **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** | | **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** |
| מתודולוגיה | critical | חוסם ייצוא | | מתודולוגיה | critical | חוסם ייצוא |
| **קול דפנה** | **critical** | **חוסם ייצוא** | | **קול דפנה** | **critical** | **חוסם ייצוא** |
| **צירוף פסיקה ל-DB** | **critical** | **חוסם ייצוא** |
| מראה מקום מלא | warning | מדווח, לא חוסם |
| תקפות סטטוס | sanity | דיווח בלבד |
## תהליך עבודה ## תהליך עבודה

View File

@@ -21,6 +21,8 @@ tools:
- mcp__legal-ai__precedent_list - mcp__legal-ai__precedent_list
- mcp__legal-ai__precedent_search_library - mcp__legal-ai__precedent_search_library
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
- mcp__legal-ai__internal_decision_upload
- mcp__legal-ai__precedent_library_upload
- mcp__legal-ai__precedent_library_get - mcp__legal-ai__precedent_library_get
- mcp__legal-ai__precedent_library_list - mcp__legal-ai__precedent_library_list
- mcp__legal-ai__precedent_extract_halachot - mcp__legal-ai__precedent_extract_halachot
@@ -70,6 +72,84 @@ tools:
כתבי ערר, תשובות, תגובות — אלה בטיפול סוכן "מנתח משפטי". כתבי ערר, תשובות, תגובות — אלה בטיפול סוכן "מנתח משפטי".
## ⚠️ חובה לקרוא — איזה כלי upload להשתמש לכל סוג פסיקה
כשאתה מעלה פסיקה לקורפוס הסמכותי, **יש שני זרמים שונים** והם **לא ניתנים להחלפה**. שגיאה כאן פוגעת בכל המערכת.
### Flowchart החלטה — איזה כלי?
```
האם ה-citation מתחיל ב-"ערר" או "בל"מ" (החלטת ועדת ערר)?
├── כן → internal_decision_upload ✅ (חובה chair_name + district)
└── לא →
האם מתחיל ב-עע"מ / בר"מ / עמ"נ / בג"ץ / ע"א / ע"פ / רע"א / רע"פ / ת"א / ת"מ
(פסיקת בית משפט מנהלי/עליון/מחוזי/שלום)?
├── כן → precedent_library_upload ✅ (external_upload)
└── לא → דווח לחיים: citation לא מוכר, אל תעלה
```
### זרם A — `precedent_library_upload` (external)
לפסיקת ערכאות שיפוטיות: עליון (בג"ץ/ע"א/רע"א/ע"פ/רע"פ/דנ"א), מנהלי (עע"מ/בר"מ/עמ"נ), מחוזי (ת"א/ת"מ), שלום.
```python
mcp__legal-ai__precedent_library_upload(
file_path="/path/to/file.pdf",
citation="עע\"מ 3911/19 פלוני נ' הוועדה המקומית רמת גן (פורסם בנבו, 12.07.2023)",
case_name="פלוני נ' הוועדה המקומית רמת גן",
court="בית המשפט העליון",
decision_date="2023-07-12",
practice_area="rishuy_uvniya", # Axis B בלבד
subject_tags=["שימוש חורג", "מגרש מסחרי"],
)
```
**הכלי שומר `source_kind='external_upload'`.** Citation guard: אם תנסה להעלות citation שמתחיל ב-"ערר" או "בל\"מ" — הכלי **ידחה** עם שגיאה ויפנה ל-`internal_decision_upload`.
### זרם B — `internal_decision_upload` (internal_committee) — **חובה לחלק מהפסיקה**
להחלטות **ועדות ערר** מכל המחוזות (ירושלים, מרכז, תל אביב, צפון, דרום, חיפה, ארצי). כולל גם ערר רגיל וגם בל"מ.
```python
mcp__legal-ai__internal_decision_upload(
file_path="/path/to/file.pdf",
case_number="ערר (ועדות ערר - תכנון ובנייה ירושלים) 1110/20",
chair_name="שרית אריאלי", # חובה!
district="ירושלים", # חובה! אחד מ-7
case_name="פלוני נ' הוועדה המקומית מודיעין",
court="ועדת הערר לתכנון ובנייה — מחוז ירושלים",
decision_date="2020-11-15",
practice_area="rishuy_uvniya", # Axis B
appeal_subtype="building_permit",
subject_tags=["שימוש חורג"],
is_binding=False, # תמיד False — שכנוע אופקי, לא חוב
)
```
**שדות חובה (הכלי דוחה בלעדיהם):**
- `file_path`
- `case_number`
- `chair_name` — בלעדיו אי-אפשר לחפש סלקטיבית לפי הרכב
- `district` — ערכים תקפים: **ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי** (גם "תל-אביב" עם מקף נקלט)
**הכלי שומר `source_kind='internal_committee'`.** DB constraint `case_law_internal_district_check` אוכף ש-`district NOT NULL` כשמדובר ב-internal_committee.
### אם chair_name או district חסר ב-PDF
- חפש בתוך הטקסט: "בפני: עו\"ד X" / "יו\"ר הוועדה: X" / "מחוז ירושלים" / שם המחוז בכותרת
- אם לא מצליח לזהות — **אל תנחש**. דווח לחיים ב-comment: "נמצא PDF של החלטת ערר ללא chair_name/district ברורים — נדרש מילוי ידני". המשך עם שאר העבודה.
### 2 שכבות חיפוש מקבילות
לאחר ההעלאות הנכונות:
| כלי | מטרה | מתי |
|-----|------|-----|
| `search_precedent_library` | חיפוש פסיקה **חיצונית** (עליון/מנהלי/מחוזי) | כל סוגיה מרכזית — חובה |
| `search_internal_decisions` | חיפוש בהחלטות **ועדות ערר** (כל המחוזות) | כשהסוגיה דיונית או כשאין הלכת עליון |
שניהם מקבלים את אותם הפילטרים: `practice_area` (Axis B), `subject_tag`, וכו'. `search_internal_decisions` מקבל בנוסף `district` ו-`chair_name`.
## תהליך עבודה ## תהליך עבודה
### שלב 1: התמצאות ### שלב 1: התמצאות
@@ -104,8 +184,8 @@ tools:
| סיווג תיק | practice_area | | סיווג תיק | practice_area |
|------------|---------------| |------------|---------------|
| 1xxx (רישוי ובניה) | `rishuy_uvniya` | | 1xxx (רישוי ובניה) | `rishuy_uvniya` |
| 8xxx (היטל השבחה) | `histael_hashbacha` | | 8xxx (היטל השבחה) | `betterment_levy` |
| 9xxx (פיצויים ס' 197) | `pitsuim_197` | | 9xxx (פיצויים ס' 197) | `compensation_197` |
אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר.
@@ -136,7 +216,7 @@ search_precedent_library(
``` ```
search_internal_decisions( search_internal_decisions(
query="...", query="...",
practice_area="histael_hashbacha", # rishuy_uvniya / betterment_levy / compensation_197 practice_area="betterment_levy", # rishuy_uvniya / betterment_levy / compensation_197
district="ירושלים", # ריק = כל המחוזות district="ירושלים", # ריק = כל המחוזות
chair_name="", # ריק = כל היו"רים; "דפנה תמיר" = דפנה בלבד (שווה ל-search_decisions) chair_name="", # ריק = כל היו"רים; "דפנה תמיר" = דפנה בלבד (שווה ל-search_decisions)
limit=5 limit=5

View File

@@ -20,6 +20,7 @@ tools:
- mcp__legal-ai__write_block - mcp__legal-ai__write_block
- mcp__legal-ai__search_decisions - mcp__legal-ai__search_decisions
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
- mcp__legal-ai__search_internal_decisions
- mcp__legal-ai__precedent_library_get - mcp__legal-ai__precedent_library_get
- mcp__legal-ai__precedent_library_list - mcp__legal-ai__precedent_library_list
- mcp__legal-ai__halacha_review - mcp__legal-ai__halacha_review
@@ -350,6 +351,26 @@ fi
חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד. חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד.
### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind`
כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה:
| source_kind | מקור | מעמד | תבנית ניסוח בבלוק י |
|-------------|------|------|----------------------|
| `external_upload` | בית משפט (עליון/מנהלי/מחוזי/שלום) | **סמכותי — מחייב או משכנע גבוה** | "בהתאם להלכת **X** ב-עע\"מ NNNN/YY, נקבע כי..." / "כפי שהבהיר בית המשפט העליון ב-בג\"ץ NNN/YY, '...'" |
| `internal_committee` (אחר) | ועדת ערר אחרת | **שכנוע אופקי בלבד — לא מחייב** | "כפי שנקבע על-ידי כב' היו\"ר **Y** במחוז Z בערר NNNN/YY, '...'. סוגיה זו עלתה בפנינו, ואנו מסכימים עם הניתוח הנ\"ל..." |
| `internal_committee` של דפנה עצמה | החלטה קודמת של דפנה | **עקביות עצמית (ג'וריספרודנציה אישית)** | "כפי שקבעתי בעבר בערר NNNN/YY, '...'. אין מקום לסטות מכך גם בעניין שלפנינו." (קול אישי "אנחנו"/"אני" — לפי מה שמופיע בקורפוס המקור) |
**עקרון CREAC (Rule + Explanation):**
- **Rule (כלל)**: רק מ-`external_upload` (פסיקת ערכאות) או מחוקקה. **אסור** להציג ועדת ערר אחרת כ"כלל מחייב".
- **Explanation (הרחבה/שכנוע)**: `internal_committee` יכול לתפוס כאן — אבל **בנפרד** מהכלל, כשכנוע נוסף.
- **אם אין הלכת עליון** ויש רק ועדת ערר תומכת — נסח: "לעת הזו, סוגיה זו טרם נדונה בערכאות עליונות. עם זאת, כפי שנקבע ב<ערר>... מצאנו את ההנמקה משכנעת ואנו אומצים אותה."
**בדיקה לפני שאתה כותב ציטוט:**
1. הוצא את ה-`source_kind` מהפלט של `search_precedent_library` או `search_internal_decisions`.
2. אם `internal_committee` — בדוק את `chair_name`. אם זו דפנה תמיר → סגנון "כפי שקבעתי בעבר". אחרת → סגנון אופקי עם ציון מחוז.
3. אל תערבב — שלוש קטגוריות שונות, שלוש תבניות שונות.
### אנטי-דפוסים — בדיקה אחרי כתיבה (חובה) ### אנטי-דפוסים — בדיקה אחרי כתיבה (חובה)
- [ ] **אין רשימות ממוספרות בתוך פסקה** (`(1)... (2)... (3)...`) — דפנה מעולם לא משתמשת - [ ] **אין רשימות ממוספרות בתוך פסקה** (`(1)... (2)... (3)...`) — דפנה מעולם לא משתמשת

View File

@@ -53,6 +53,7 @@ mcp = FastMCP(
from legal_mcp.tools import ( # noqa: E402 from legal_mcp.tools import ( # noqa: E402
cases, documents, search, drafting, workflow, precedents, cases, documents, search, drafting, workflow, precedents,
precedent_library as plib, precedent_library as plib,
internal_decisions as int_tools,
) )
@@ -662,6 +663,46 @@ async def internal_decision_enrich(
return _json.dumps(result, ensure_ascii=False, indent=2) return _json.dumps(result, ensure_ascii=False, indent=2)
@mcp.tool()
async def internal_decision_upload(
file_path: str,
case_number: str,
chair_name: str,
district: str,
case_name: str = "",
court: str = "",
decision_date: str = "",
practice_area: str = "",
appeal_subtype: str = "",
subject_tags: list[str] | None = None,
summary: str = "",
is_binding: bool = False,
) -> str:
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
שדות חובה: file_path, case_number, chair_name, district.
שמירת ההחלטה עוברת דרך ingest_internal_decision — תויג source_kind='internal_committee' אוטומטית.
district תקין: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.
בניגוד ל-precedent_library_upload (שתמיד שומר external_upload),
הכלי הזה הוא הנתיב המוסמך להחלטות ועדת ערר ומכריח chair_name+district.
"""
return await int_tools.internal_decision_upload(
file_path=file_path,
case_number=case_number,
chair_name=chair_name,
district=district,
case_name=case_name,
court=court,
decision_date=decision_date,
practice_area=practice_area,
appeal_subtype=appeal_subtype,
subject_tags=subject_tags,
summary=summary,
is_binding=is_binding,
)
@mcp.tool() @mcp.tool()
async def record_chair_feedback( async def record_chair_feedback(
case_number: str, case_number: str,

View File

@@ -2,14 +2,34 @@
Two orthogonal axes used to separate legal domains across the system: Two orthogonal axes used to separate legal domains across the system:
practice_area — top-level domain (multi-tenant axis). Examples: practice_area — top-level domain. **Two taxonomies coexist** (see below).
appeals_committee, national_insurance, labor_law. appeal_subtype — refines within a domain.
appeal_subtype — refines within a domain. For appeals_committee:
building_permit (1xxx), betterment_levy (8xxx),
compensation_197 (9xxx), unknown.
Both columns are denormalized into documents/chunks/decisions/style_corpus ⚠️ TWO TAXONOMIES — DO NOT CONFUSE
so vector searches can filter cheaply. ==================================
A. **Multi-tenant axis** (legacy, used in routing logic):
- ``appeals_committee`` — the legal-ai instance for Daphna's committee
- ``national_insurance`` — future / hypothetical other tenants
- ``labor_law`` — future
When this axis is used, ``appeal_subtype`` carries the actual domain:
``building_permit`` (1xxx), ``betterment_levy`` (8xxx),
``compensation_197`` (9xxx).
B. **Domain axis** (DB columns ``case_law.practice_area``,
``cases.practice_area`` — what tests, validators, and CHECK constraints
actually use):
- ``rishuy_uvniya`` — רישוי ובנייה (1xxx)
- ``betterment_levy`` — היטל השבחה (8xxx)
- ``compensation_197`` — פיצויים סעיף 197 (9xxx)
Use ``to_db_practice_area(multi_tenant_pa, appeal_subtype)`` to convert
from axis A to axis B before writing to the DB.
Background: TaskMaster #30 (sub-bug ב) — many ``case_law`` rows stored
``appeals_committee`` (axis A) where they should have stored a domain
value (axis B). The migration backfill plus CHECK constraints close the
gap, and this module now validates **both** namespaces.
""" """
from __future__ import annotations from __future__ import annotations
@@ -18,12 +38,23 @@ import re
# ── Enums ────────────────────────────────────────────────────────── # ── Enums ──────────────────────────────────────────────────────────
PRACTICE_AREAS: set[str] = { # Multi-tenant axis (legacy)
MULTI_TENANT_PRACTICE_AREAS: set[str] = {
"appeals_committee", "appeals_committee",
"national_insurance", "national_insurance",
"labor_law", "labor_law",
} }
# Domain axis (matches DB constraints on case_law/cases)
DOMAIN_PRACTICE_AREAS: set[str] = {
"rishuy_uvniya",
"betterment_levy",
"compensation_197",
}
# Union — what ``validate()`` accepts for backward-compat
PRACTICE_AREAS: set[str] = MULTI_TENANT_PRACTICE_AREAS | DOMAIN_PRACTICE_AREAS
APPEALS_COMMITTEE_SUBTYPES: set[str] = { APPEALS_COMMITTEE_SUBTYPES: set[str] = {
"building_permit", "building_permit",
"betterment_levy", "betterment_levy",
@@ -38,8 +69,42 @@ SUBTYPES_BY_AREA: dict[str, set[str]] = {
"appeals_committee": APPEALS_COMMITTEE_SUBTYPES, "appeals_committee": APPEALS_COMMITTEE_SUBTYPES,
"national_insurance": {"unknown"}, "national_insurance": {"unknown"},
"labor_law": {"unknown"}, "labor_law": {"unknown"},
# Domain values — subtype is implicit in the value itself
"rishuy_uvniya": {"building_permit", "unknown"},
"betterment_levy": {"betterment_levy", "unknown"},
"compensation_197": {"compensation_197", "unknown"},
} }
# Mapping: (multi_tenant_pa, appeal_subtype) → domain_pa
_SUBTYPE_TO_DOMAIN: dict[str, str] = {
"building_permit": "rishuy_uvniya",
"betterment_levy": "betterment_levy",
"compensation_197": "compensation_197",
}
def to_db_practice_area(practice_area: str, appeal_subtype: str = "") -> str:
"""Convert a multi-tenant practice_area + appeal_subtype to the
domain value stored in DB columns (case_law/cases).
Returns ``""`` when the input cannot be mapped — callers should
handle this rather than letting ``""`` propagate silently to the DB.
Examples:
>>> to_db_practice_area("appeals_committee", "building_permit")
'rishuy_uvniya'
>>> to_db_practice_area("rishuy_uvniya")
'rishuy_uvniya'
>>> to_db_practice_area("appeals_committee")
''
"""
pa = (practice_area or "").strip()
if pa in DOMAIN_PRACTICE_AREAS:
return pa
if pa == "appeals_committee":
return _SUBTYPE_TO_DOMAIN.get((appeal_subtype or "").strip(), "")
return ""
# ── Derivation ───────────────────────────────────────────────────── # ── Derivation ─────────────────────────────────────────────────────

View File

@@ -0,0 +1,104 @@
"""MCP tools for the Internal Decisions corpus.
Decisions of appeals committees (ועדות ערר) live in the same physical
``case_law`` table as court rulings but are distinguished by
``source_kind='internal_committee'`` and must carry ``chair_name`` +
``district``.
The existing ``precedent_library_upload`` MCP tool always stores
``source_kind='external_upload'`` and does not accept chair/district —
which is why **44+ existing appeals-committee decisions were tagged
wrong**. This wrapper is the authoritative ingestion path for committee
decisions and enforces the required metadata at the tool boundary.
"""
from __future__ import annotations
import json
from legal_mcp.services import internal_decisions as int_svc
# Valid Hebrew district names (matches _COURT_TO_DISTRICT in service)
VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב", "צפון", "דרום", "חיפה", "ארצי"}
def _ok(payload) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
def _err(msg: str) -> str:
return json.dumps({"error": msg}, ensure_ascii=False)
async def internal_decision_upload(
file_path: str,
case_number: str,
chair_name: str,
district: str,
case_name: str = "",
court: str = "",
decision_date: str = "",
practice_area: str = "",
appeal_subtype: str = "",
subject_tags: list[str] | None = None,
summary: str = "",
is_binding: bool = False,
) -> str:
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
Required: file_path, case_number, chair_name, district.
The tool enforces chair_name+district so the record cannot be saved
in the broken legacy mode (external_upload with empty chair/district).
Args:
file_path: נתיב מלא לקובץ PDF/DOCX/RTF/TXT/MD.
case_number: מספר הערר ("ערר (ועדות ערר - תכנון ובנייה ירושלים) 1110/20 ...").
chair_name: שם יו"ר הוועדה (חובה).
district: מחוז (ירושלים/מרכז/תל אביב/צפון/דרום/חיפה/ארצי) — חובה.
case_name: שם קצר.
court: ערכאה ("ועדת הערר לתכנון ובנייה — מחוז ירושלים").
decision_date: ISO date (YYYY-MM-DD), אופציונלי.
practice_area: rishuy_uvniya / betterment_levy / compensation_197.
appeal_subtype: building_permit / וכו'.
subject_tags: תגיות נושא.
is_binding: בד"כ False (ועדת ערר לא מחייבת ועדה אחרת — שכנוע אופקי).
Returns: JSON עם case_law_id, מספר chunks, halachot_pending.
"""
if not file_path.strip():
return _err("file_path חובה")
if not case_number.strip():
return _err("case_number חובה")
if not chair_name.strip():
return _err(
"chair_name חובה. החלטות ועדת ערר חייבות שם יו\"ר — "
"בלעדיו ההחלטה לא ניתנת לחיפוש סלקטיבי לפי הרכב."
)
if not district.strip():
return _err(
"district חובה. ערכים תקפים: " + ", ".join(sorted(VALID_DISTRICTS))
)
if district.strip() not in VALID_DISTRICTS:
return _err(
f"district לא תקין: {district!r}. ערכים תקפים: "
+ ", ".join(sorted(VALID_DISTRICTS))
)
try:
result = await int_svc.ingest_internal_decision(
case_number=case_number,
case_name=case_name,
court=court,
decision_date=decision_date or None,
chair_name=chair_name,
district=district,
practice_area=practice_area,
appeal_subtype=appeal_subtype,
subject_tags=subject_tags or [],
summary=summary,
is_binding=is_binding,
file_path=file_path,
)
except Exception as e:
return _err(str(e))
return _ok(result)

View File

@@ -63,6 +63,18 @@ async def precedent_library_upload(
""" """
if not citation.strip(): if not citation.strip():
return _err("citation חובה") return _err("citation חובה")
# Citation guard: appeals-committee decisions must go through
# internal_decision_upload (with chair_name + district). The legacy
# path always stored source_kind='external_upload' and left
# chair_name/district empty — see TaskMaster #30(ב).
_norm = citation.strip()
_committee_prefixes = ("ערר ", "ערר(", "ערר ", "בל\"מ ", "בל\"מ(", "ARAR ")
if any(_norm.startswith(p) for p in _committee_prefixes):
return _err(
"ציטוט שמתחיל ב-'ערר' או 'בל\"מ' הוא החלטת ועדת ערר. "
"השתמש ב-internal_decision_upload (דורש chair_name + district), "
"לא ב-precedent_library_upload."
)
try: try:
result = await precedent_library.ingest_precedent( result = await precedent_library.ingest_precedent(
file_path=file_path, file_path=file_path,