diff --git a/.claude/agents/HEARTBEAT.md b/.claude/agents/HEARTBEAT.md index 476a999..8eee52c 100644 --- a/.claude/agents/HEARTBEAT.md +++ b/.claude/agents/HEARTBEAT.md @@ -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 הרשמי | פעולה | איפה ב-skill | diff --git a/.claude/agents/legal-analyst.md b/.claude/agents/legal-analyst.md index fe6a93e..a5b20d9 100644 --- a/.claude/agents/legal-analyst.md +++ b/.claude/agents/legal-analyst.md @@ -63,6 +63,26 @@ tools: - חוקי תמ"א 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 סוגי פריטים מחולצים | סוג (claim_type) | מה זה | מי אמר | @@ -181,8 +201,8 @@ tools: | סיווג תיק | practice_area | |------------|---------------| | 1xxx (רישוי ובניה) | `rishuy_uvniya` | -| 8xxx (היטל השבחה) | `histael_hashbacha` | -| 9xxx (פיצויים ס' 197) | `pitsuim_197` | +| 8xxx (היטל השבחה) | `betterment_levy` | +| 9xxx (פיצויים ס' 197) | `compensation_197` | אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת. diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index 36634fc..9e2e074 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -18,6 +18,8 @@ tools: - mcp__legal-ai__list_chair_feedback - mcp__legal-ai__search_case_documents - 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__processing_status - mcp__legal-ai__get_metrics @@ -78,6 +80,48 @@ tools: | `docs/daphna-procedural-patterns.md` | תבניות פרוצדורליות (החלטת ביניים, חזרה לשמאי) | CEO + writer (8xxx בלבד) | | `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 | תפקיד | @@ -597,7 +641,7 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md | `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) | | `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` | | `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 | | `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה | | `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) | diff --git a/.claude/agents/legal-qa.md b/.claude/agents/legal-qa.md index 27a52a3..6f4c6e5 100644 --- a/.claude/agents/legal-qa.md +++ b/.claude/agents/legal-qa.md @@ -15,7 +15,9 @@ tools: - mcp__legal-ai__workflow_status - mcp__legal-ai__search_case_documents - mcp__legal-ai__search_precedent_library + - mcp__legal-ai__search_internal_decisions - mcp__legal-ai__precedent_library_get + - mcp__legal-ai__precedent_list - mcp__legal-ai__halacha_review --- @@ -145,6 +147,39 @@ tools: - האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)? - **ציטוטי פסיקה חיצונית בבלוק י** — לכל ציטוט (`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` — אם תוצאה = קבלה) - האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה? - האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה? @@ -165,6 +200,9 @@ tools: | **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** | | מתודולוגיה | critical | חוסם ייצוא | | **קול דפנה** | **critical** | **חוסם ייצוא** | +| **צירוף פסיקה ל-DB** | **critical** | **חוסם ייצוא** | +| מראה מקום מלא | warning | מדווח, לא חוסם | +| תקפות סטטוס | sanity | דיווח בלבד | ## תהליך עבודה diff --git a/.claude/agents/legal-researcher.md b/.claude/agents/legal-researcher.md index 851726e..696e309 100644 --- a/.claude/agents/legal-researcher.md +++ b/.claude/agents/legal-researcher.md @@ -21,6 +21,8 @@ tools: - mcp__legal-ai__precedent_list - mcp__legal-ai__precedent_search_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_list - 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: התמצאות @@ -104,8 +184,8 @@ tools: | סיווג תיק | practice_area | |------------|---------------| | 1xxx (רישוי ובניה) | `rishuy_uvniya` | -| 8xxx (היטל השבחה) | `histael_hashbacha` | -| 9xxx (פיצויים ס' 197) | `pitsuim_197` | +| 8xxx (היטל השבחה) | `betterment_levy` | +| 9xxx (פיצויים ס' 197) | `compensation_197` | אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. @@ -136,7 +216,7 @@ search_precedent_library( ``` search_internal_decisions( query="...", - practice_area="histael_hashbacha", # rishuy_uvniya / betterment_levy / compensation_197 + practice_area="betterment_levy", # rishuy_uvniya / betterment_levy / compensation_197 district="ירושלים", # ריק = כל המחוזות chair_name="", # ריק = כל היו"רים; "דפנה תמיר" = דפנה בלבד (שווה ל-search_decisions) limit=5 diff --git a/.claude/agents/legal-writer.md b/.claude/agents/legal-writer.md index 93e582f..d212b52 100644 --- a/.claude/agents/legal-writer.md +++ b/.claude/agents/legal-writer.md @@ -20,6 +20,7 @@ tools: - mcp__legal-ai__write_block - mcp__legal-ai__search_decisions - mcp__legal-ai__search_precedent_library + - mcp__legal-ai__search_internal_decisions - mcp__legal-ai__precedent_library_get - mcp__legal-ai__precedent_library_list - mcp__legal-ai__halacha_review @@ -350,6 +351,26 @@ fi חפש לפי `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)...`) — דפנה מעולם לא משתמשת diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index abb72fc..54fbe74 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -53,6 +53,7 @@ mcp = FastMCP( from legal_mcp.tools import ( # noqa: E402 cases, documents, search, drafting, workflow, precedents, 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) +@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() async def record_chair_feedback( case_number: str, diff --git a/mcp-server/src/legal_mcp/services/practice_area.py b/mcp-server/src/legal_mcp/services/practice_area.py index 934958c..6fbbdb3 100644 --- a/mcp-server/src/legal_mcp/services/practice_area.py +++ b/mcp-server/src/legal_mcp/services/practice_area.py @@ -2,14 +2,34 @@ Two orthogonal axes used to separate legal domains across the system: - practice_area — top-level domain (multi-tenant axis). Examples: - appeals_committee, national_insurance, labor_law. - appeal_subtype — refines within a domain. For appeals_committee: - building_permit (1xxx), betterment_levy (8xxx), - compensation_197 (9xxx), unknown. + practice_area — top-level domain. **Two taxonomies coexist** (see below). + appeal_subtype — refines within a domain. -Both columns are denormalized into documents/chunks/decisions/style_corpus -so vector searches can filter cheaply. +⚠️ TWO TAXONOMIES — DO NOT CONFUSE +================================== + +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 @@ -18,12 +38,23 @@ import re # ── Enums ────────────────────────────────────────────────────────── -PRACTICE_AREAS: set[str] = { +# Multi-tenant axis (legacy) +MULTI_TENANT_PRACTICE_AREAS: set[str] = { "appeals_committee", "national_insurance", "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] = { "building_permit", "betterment_levy", @@ -38,8 +69,42 @@ SUBTYPES_BY_AREA: dict[str, set[str]] = { "appeals_committee": APPEALS_COMMITTEE_SUBTYPES, "national_insurance": {"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 ───────────────────────────────────────────────────── diff --git a/mcp-server/src/legal_mcp/tools/internal_decisions.py b/mcp-server/src/legal_mcp/tools/internal_decisions.py new file mode 100644 index 0000000..4105444 --- /dev/null +++ b/mcp-server/src/legal_mcp/tools/internal_decisions.py @@ -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) diff --git a/mcp-server/src/legal_mcp/tools/precedent_library.py b/mcp-server/src/legal_mcp/tools/precedent_library.py index f8b470e..6bfd30a 100644 --- a/mcp-server/src/legal_mcp/tools/precedent_library.py +++ b/mcp-server/src/legal_mcp/tools/precedent_library.py @@ -63,6 +63,18 @@ async def precedent_library_upload( """ if not citation.strip(): 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: result = await precedent_library.ingest_precedent( file_path=file_path,