Compare commits
79 Commits
081c7fb17a
...
ui-rewrite
| Author | SHA1 | Date | |
|---|---|---|---|
| ed8502d46b | |||
| 0fef20e272 | |||
| ca6ec48580 | |||
| 4e418787cf | |||
| fdd12c6726 | |||
| e34d217345 | |||
| 6b8f002596 | |||
| aa0e608a4a | |||
| 916360e9b2 | |||
| cbe9d60901 | |||
| fb1f73fa25 | |||
| ac0a5ee30b | |||
| e483eba1a9 | |||
| d8a537e7aa | |||
| 75ea6825b2 | |||
| 10540a38b4 | |||
| b67dc47dc7 | |||
| 9fcf4f2dc7 | |||
| 03b25bc273 | |||
| d0daa0efe8 | |||
| 51064f3a03 | |||
|
|
0ee8e723bd | ||
|
|
64724656af | ||
| a8b79822bf | |||
| 0c4886afe6 | |||
| 753fe0d57d | |||
| ffa089e1df | |||
| 5cb0be473c | |||
| ea3ef5963e | |||
| 3e0221ccec | |||
| fcb2e1a325 | |||
| d5164e2875 | |||
| 858333b386 | |||
| 32f18de049 | |||
| ecda95d610 | |||
| b409f1c7eb | |||
| 3f759d3610 | |||
| 63c9ca184b | |||
| bfcbb6708a | |||
| 22e819363e | |||
| 4d674bf475 | |||
| 6aaca14e31 | |||
| bc72a83a71 | |||
| d8e888ad6a | |||
| 2d265d2f0e | |||
| 6a62edbdb4 | |||
| 5a8d5cac0a | |||
| b2f60d51f4 | |||
| e1d2e18ea8 | |||
| 22196f48cb | |||
| 4df2040a40 | |||
| 85880c482e | |||
| c83dcd660e | |||
| e6293250aa | |||
| 6a93292f56 | |||
| 65e78f493c | |||
| f4dd4f7134 | |||
| 4574987a69 | |||
| 9ba489ee21 | |||
| 96ea54dc6e | |||
| 328436f56d | |||
| 911c797eb2 | |||
| d5ccf03e4c | |||
| bacb330a2a | |||
| e5dc037088 | |||
| 8db06c9ac6 | |||
| 52ee3419d3 | |||
| 9e7492e761 | |||
| 40406b5fde | |||
| 561a4f7bcf | |||
| 10071d7f18 | |||
| 5fc52ce530 | |||
| dc6026100c | |||
| 0dfb42ab00 | |||
| 0593fe9b01 | |||
| cb41867bc9 | |||
| 324807ff1d | |||
| 316dd2aefb | |||
| 59bb471368 |
90
.claude/agents/HEARTBEAT.md
Normal file
90
.claude/agents/HEARTBEAT.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# HEARTBEAT.md — רשימת ביצוע לכל ריצה
|
||||
|
||||
## שפה — כלל עליון
|
||||
|
||||
**כל הפלט שלך חייב להיות בעברית בלבד.** זה כולל:
|
||||
- Comments ב-Paperclip
|
||||
- הודעות סטטוס
|
||||
- תיאורי שגיאות
|
||||
- סיכומים ודיווחים
|
||||
- חשיבה פנימית (thinking)
|
||||
|
||||
אין יוצאים מן הכלל. גם שמות tools, פקודות, ונתיבי קבצים — ההסבר סביבם בעברית.
|
||||
|
||||
---
|
||||
|
||||
הרץ את הרשימה הזו בכל heartbeat.
|
||||
|
||||
## 1. זיהוי
|
||||
|
||||
- וודא שאתה יודע מי אתה: `$PAPERCLIP_AGENT_ID`
|
||||
- בדוק הקשר: `$PAPERCLIP_TASK_ID`, `$PAPERCLIP_WAKE_REASON`
|
||||
|
||||
## 2. בדוק תיבת דואר
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" "$PAPERCLIP_API_URL/api/agents/me/inbox-lite"
|
||||
```
|
||||
|
||||
- תעדוף: `in_progress` קודם, אחר כך `todo`
|
||||
- אם `PAPERCLIP_TASK_ID` מוגדר — תעדף אותו
|
||||
|
||||
## 3. Checkout ועבודה
|
||||
|
||||
```bash
|
||||
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
"$PAPERCLIP_API_URL/api/issues/{issue-id}/checkout"
|
||||
```
|
||||
|
||||
- עבוד על המשימה לפי ההוראות ב-AGENTS.md שלך
|
||||
- השתמש בכלים המשפטיים (legal-ai MCP)
|
||||
|
||||
## 4. דיווח — חובה!
|
||||
|
||||
**לפני שאתה מסיים, תמיד:**
|
||||
|
||||
פרסם comment על ה-issue:
|
||||
```bash
|
||||
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" \
|
||||
-d '{"body": "סיכום העבודה..."}'
|
||||
```
|
||||
|
||||
עדכן סטטוס issue:
|
||||
```bash
|
||||
curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$PAPERCLIP_API_URL/api/issues/{issue-id}" \
|
||||
-d '{"status": "done"}'
|
||||
```
|
||||
|
||||
## 5. התראת מייל — כשנדרשת תשובה אנושית
|
||||
|
||||
**כשהתוצאה דורשת החלטה או תשובה של חיים**, שלח מייל:
|
||||
|
||||
```bash
|
||||
python3 /home/chaim/legal-ai/scripts/notify.py \
|
||||
"נדרשת תשובתך — [תיאור קצר]" \
|
||||
"תוכן ההודעה עם סיכום מה נדרש"
|
||||
```
|
||||
|
||||
**מתי לשלוח — תמיד:**
|
||||
- **סיום כל משימה** — עם סיכום קצר של מה בוצע
|
||||
- בקשה לקביעת תוצאה (דחייה/קבלה/חלקית)
|
||||
- בקשה לאישור כיוון נימוק
|
||||
- דוח QA שנכשל (צריך החלטה על תיקונים)
|
||||
- החלטה מוכנה לביקורת דפנה
|
||||
- כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד
|
||||
- שגיאה שלא ניתן לפתור ללא התערבות
|
||||
|
||||
**מתי לא לשלוח:**
|
||||
- עדכוני סטטוס ביניים (רק בסיום)
|
||||
- שגיאות טכניות שאפשר לפתור לבד
|
||||
|
||||
## 6. Release
|
||||
|
||||
```bash
|
||||
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
"$PAPERCLIP_API_URL/api/issues/{issue-id}/release"
|
||||
```
|
||||
215
.claude/agents/legal-analyst.md
Normal file
215
.claude/agents/legal-analyst.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: "legal-analyst"
|
||||
description: "מנתח ומחקר משפטי — חילוץ טענות, ניתוח אסטרטגי, זיהוי חוזקות/חולשות, והפקת שאלות מחקר ממוקדות"
|
||||
model: "claude-opus-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_list
|
||||
- mcp__legal-ai__case_update
|
||||
- mcp__legal-ai__document_list
|
||||
- mcp__legal-ai__document_get_text
|
||||
- mcp__legal-ai__extract_claims
|
||||
- mcp__legal-ai__get_claims
|
||||
- mcp__legal-ai__search_case_documents
|
||||
- mcp__legal-ai__search_decisions
|
||||
- mcp__legal-ai__find_similar_cases
|
||||
- mcp__legal-ai__workflow_status
|
||||
- mcp__legal-ai__processing_status
|
||||
---
|
||||
|
||||
# מנתח ומחקר משפטי — סוכן ניתוח אסטרטגי והפקת שאלות מחקר
|
||||
|
||||
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיקי ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות אסטרטגיה משפטית, ולהפיק שאלות מחקר ממוקדות.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## תחומי התמחות
|
||||
|
||||
הסוכן ממוקד בתחומים הבאים:
|
||||
- חוק התכנון והבניה, התשכ"ה-1965 וכל התקנות שמכוחו
|
||||
- חוק המקרקעין, התשכ"ט-1969 וכל התקנות שמכוחו
|
||||
- התוספת השלישית לחוק התכנון והבניה (היטל השבחה)
|
||||
- תקנות התכנון והבניה (חישוב שטחים, בקשה להיתר, סטיה ניכרת, היטל השבחה)
|
||||
- תקנות המקרקעין (ניהול ורישום)
|
||||
- חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית
|
||||
- ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין)
|
||||
|
||||
## הבחנה קריטית — 3 סוגי פריטים מחולצים
|
||||
|
||||
| סוג (claim_type) | מה זה | מי אמר |
|
||||
|-------------------|--------|---------|
|
||||
| **claim** | טענות — מה הצד טוען | בד"כ עוררים (appellant) |
|
||||
| **response** | תשובות — מה עונים לטענה | בד"כ ועדה מקומית (committee) או משיבים |
|
||||
| **reply** | תגובות — תשובות לתשובות | בד"כ מבקשת ההיתר (permit_applicant) |
|
||||
|
||||
## סוגי מסמכים — מה לחלץ ומה לא
|
||||
|
||||
| סוג מסמך | מה לחלץ | claim_type |
|
||||
|-----------|----------|------------|
|
||||
| כתב ערר | **טענות** — מה העוררים טוענים | claim |
|
||||
| כתב תשובה | **תשובות** — מה המשיבים/ועדה עונים | response |
|
||||
| תגובה / השלמת טיעון | **תגובות** — תשובות לתשובות | reply |
|
||||
| פסיקה / תכנית / פרוטוקול / היתר | **אל תחלץ כלום** — מסמכי רקע בלבד | — |
|
||||
|
||||
## תהליך עבודה — 4 שלבים
|
||||
|
||||
### שלב 1: קליטה וזיהוי
|
||||
1. קרא פרטי התיק (`case_get`)
|
||||
2. קרא רשימת מסמכים (`document_list`)
|
||||
3. זהה:
|
||||
- **סוג ההליך**: ערר תכנוני, ערר היטל השבחה, ערעור מנהלי וכד'
|
||||
- **הערכאה/הגוף**: ועדת ערר מחוזית, בית משפט לעניינים מנהליים וכד'
|
||||
- **הצדדים**: מי העורר, מי המשיב, מי צד ג'
|
||||
- **המסגרת הנורמטיבית**: חוקים, תקנות, תכניות רלוונטיות (רק מהמסמכים)
|
||||
4. חלץ טענות/תשובות/תגובות (`extract_claims` עם doc_type ו-party_hint מתאימים)
|
||||
5. וודא שכל פריט מסווג ל-claim_type הנכון
|
||||
|
||||
### שלב 2: ניתוח מעמיק
|
||||
הצג במבנה הבא:
|
||||
|
||||
**צד מיוצג**: ועדת הערר (יו"ר — עו"ד דפנה תמיר). אנחנו צד ניטרלי שמכריע.
|
||||
|
||||
**רקע דיוני**: סוג ההליך, מספר תיק, תאריכים מרכזיים, היסטוריה דיונית, תכניות רלוונטיות.
|
||||
|
||||
**עובדות מוסכמות**: רשימה של עובדות שאין עליהן מחלוקת. רק עובדות מהמסמכים.
|
||||
|
||||
**עובדות שנויות במחלוקת**: רשימה של עובדות שהצדדים חלוקים לגביהן — פרט מה כל צד טוען.
|
||||
|
||||
### שלב 3: טענות סף, סוגיות להכרעה ואסטרטגיה
|
||||
|
||||
**טענות סף** (אם קיימות):
|
||||
חוסר סמכות, שיהוי, התיישנות, אי-מיצוי הליכים, חוסר יריבות, מעשה בית דין — הצג כל אחת עם עמדת שני הצדדים. אם אין — כתוב: "לא זוהו טענות סף."
|
||||
|
||||
**סוגיות להכרעה** — לכל סוגיה מרכזית:
|
||||
1. **כותרת הסוגיה** — ניסוח תמציתי ומדויק
|
||||
2. **טענה (claim)** — מה העוררים טוענים, על מה מסתמכים
|
||||
3. **תשובה (response)** — מה הוועדה/משיבים עונים
|
||||
4. **תגובה (reply)** — מה המבקשת מגיבה (אם קיימת)
|
||||
5. **ניתוח אסטרטגי**:
|
||||
- **חוזקות** — מה חזק בכל צד? מה מבוסס היטב?
|
||||
- **חולשות** — מה חלש? מה לא מגובה בראיות?
|
||||
- **הזדמנויות** — איפה יש פתח? מה הוועדה יכולה להישען עליו?
|
||||
6. **שאלות משפטיות** — צמד שאלות (ראה שלב 4)
|
||||
7. **עמדת ועדת הערר** — שדה ריק שיו"ר הוועדה ימלא ידנית. **חובה להוסיף לכל סוגיה!** עמדה זו תשמש כהנחיה מחייבת לסוכן הכתיבה.
|
||||
|
||||
### שלב 4: הפקת שאלות מחקר
|
||||
|
||||
לכל סוגיה (כולל טענות סף), נסח **בדיוק שתי שאלות מחקר**:
|
||||
|
||||
**שאלה 1 — עקרונית (שאלת "האם")**:
|
||||
בודקת עיקרון משפטי כללי בתחום התכנון והבניה.
|
||||
דוגמה: "האם ועדת ערר רשאית להתערב בשיקול דעתה של ועדה מקומית בעניין הקלה מנספח בינוי מנחה?"
|
||||
|
||||
**שאלה 2 — יישומית (שאלת "מהם"/"כיצד"/"באילו תנאים")**:
|
||||
מיישמת את העיקרון על נסיבות המקרה.
|
||||
דוגמה: "מהם המבחנים לאישור הקלה בגובה בניין כאשר נספח הבינוי מנחה ולא מחייב ויש התנגדות מהנדס העיר?"
|
||||
|
||||
### כללים לשאלות מחקר
|
||||
- ניתנות למחקר — אפשר למצוא תשובה בפסיקה, חקיקה, או ספרות
|
||||
- צמודות לסוגיה ולנסיבות התיק — לא כלליות
|
||||
- לא שאלות שהתשובה כבר במסמכי התיק
|
||||
- **לא להמציא פסיקה** — אם יש אזכור במסמכי התיק, ניתן להתייחס. אם לא — נסח ללא הפניה
|
||||
- שימוש במונחים מקובלים בפסיקה הישראלית (מתאים לחיפוש ב-nevo/law-mate)
|
||||
|
||||
## שלב 5: חיפוש פנימי בקורפוס
|
||||
חפש תקדימים רלוונטיים בקורפוס הפנימי:
|
||||
- `search_decisions` — בהחלטות קודמות של דפנה
|
||||
- `find_similar_cases` — תיקים דומים
|
||||
הוסף תוצאות רלוונטיות תחת כל סוגיה כ-"תקדימים מהקורפוס הפנימי".
|
||||
|
||||
## שלב 6: שמירה ודיווח — חובה!
|
||||
|
||||
1. **שמור** את הפלט המלא:
|
||||
```
|
||||
{case_dir}/documents/research/analysis-and-research.md
|
||||
```
|
||||
|
||||
2. **פרסם comment** ב-Paperclip עם סיכום:
|
||||
- כמה טענות, תשובות ותגובות חולצו
|
||||
- הסוגיות המרכזיות (3-5 כותרות)
|
||||
- כמה שאלות מחקר הופקו
|
||||
- המלצה לשלב הבא
|
||||
|
||||
3. **עדכן סטטוס** (`case_update` עם status = `documents_ready`)
|
||||
|
||||
4. **שלח מייל**:
|
||||
```bash
|
||||
python3 /home/chaim/legal-ai/scripts/notify.py \
|
||||
"ניתוח ומחקר הושלמו — ערר {case_number}" \
|
||||
"סיכום: X סוגיות זוהו, Y שאלות מחקר הופקו. נדרשת ביקורתך לפני המשך."
|
||||
```
|
||||
|
||||
## מבנה הפלט המלא — analysis-and-research.md
|
||||
|
||||
```markdown
|
||||
# ניתוח ומחקר משפטי — ערר {case_number}
|
||||
תאריך: {date}
|
||||
|
||||
## 1. צד מיוצג
|
||||
ועדת הערר לתכנון ובניה, מחוז ירושלים (יו"ר: עו"ד דפנה תמיר)
|
||||
|
||||
## 2. רקע דיוני
|
||||
...
|
||||
|
||||
## 3. עובדות מוסכמות
|
||||
1. ...
|
||||
2. ...
|
||||
|
||||
## 4. עובדות שנויות במחלוקת
|
||||
1. ...
|
||||
|
||||
## 5. טענות סף
|
||||
[אם קיימות — כולל שאלות משפטיות + עמדת ועדת הערר לכל טענה]
|
||||
|
||||
## 6. סוגיות להכרעה
|
||||
|
||||
### סוגיה 1: [כותרת]
|
||||
**טענה (claim):** ...
|
||||
**תשובה (response):** ...
|
||||
**תגובה (reply):** ...
|
||||
|
||||
**ניתוח אסטרטגי:**
|
||||
- חוזקות: ...
|
||||
- חולשות: ...
|
||||
- הזדמנויות: ...
|
||||
|
||||
**שאלות משפטיות:**
|
||||
1. [שאלה עקרונית — "האם..."]
|
||||
2. [שאלה יישומית — "מהם..."]
|
||||
|
||||
**חיפוש תקדימים:**
|
||||
- nevo (קלאסי): "ביטוי" ו "ביטוי" ו "ועדת ערר"
|
||||
- nevo AI / law-mate: [השאלות המשפטיות מלמעלה — שאלה עקרונית + יישומית]
|
||||
|
||||
**חקיקה רלוונטית:**
|
||||
- סעיף X לחוק...
|
||||
|
||||
**תקדימים מהקורפוס הפנימי:**
|
||||
- [אם נמצאו]
|
||||
|
||||
**עמדת ועדת הערר:**
|
||||
[ימולא ע"י יו"ר הוועדה — עמדה/הנחיה לגבי סוגיה זו שתשמש את סוכן הכתיבה]
|
||||
|
||||
---
|
||||
|
||||
### סוגיה 2: ...
|
||||
|
||||
## 7. מסקנות
|
||||
סיכום האסטרטגיה, נקודות חוזק, סיכונים, סדר עדיפויות.
|
||||
```
|
||||
|
||||
## כללים קריטיים
|
||||
|
||||
1. **נאמנות למקור** — כל טענה חייבת לשקף את מה שנכתב, לא לפרש
|
||||
2. **לא לחלץ מפסיקה/פרוטוקולים/תכניות** — אלה מסמכי רקע בלבד
|
||||
3. **גוף שלישי** — כל טענה בגוף שלישי גם אם המקור בגוף ראשון
|
||||
4. **לא להמציא** — לא פסיקה, לא ציטוטים, לא מספרי תיקים שלא מופיעים במסמכים
|
||||
5. **שאלות מחקר הן התוצר המרכזי** — הקדש להן תשומת לב מיוחדת
|
||||
6. **אם חסר מידע** — ציין במפורש ובקש להעלות מסמכים נוספים
|
||||
186
.claude/agents/legal-ceo.md
Normal file
186
.claude/agents/legal-ceo.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: "legal-ceo"
|
||||
description: "עוזר משפטי — מנהל תהליך כתיבת החלטות, מתזמר סוכנים, מפקח על התקדמות"
|
||||
model: "claude-sonnet-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_list
|
||||
- mcp__legal-ai__case_update
|
||||
- mcp__legal-ai__document_list
|
||||
- mcp__legal-ai__get_claims
|
||||
- mcp__legal-ai__workflow_status
|
||||
- mcp__legal-ai__processing_status
|
||||
- mcp__legal-ai__get_metrics
|
||||
- mcp__legal-ai__set_outcome
|
||||
- mcp__legal-ai__approve_direction
|
||||
- mcp__legal-ai__brainstorm_directions
|
||||
- mcp__legal-ai__validate_decision
|
||||
- mcp__legal-ai__export_docx
|
||||
---
|
||||
|
||||
# עוזר משפטי — מנהל תהליך כתיבת החלטות
|
||||
|
||||
אתה מנהל תהליך כתיבת החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים. יו"ר הוועדה היא עו"ד דפנה תמיר.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## תפקידך
|
||||
|
||||
אתה מתזמר את כל תהליך כתיבת ההחלטה. אתה לא כותב בעצמך — אתה מנהל את הסוכנים שעושים את העבודה ומוודא שהתהליך מתקדם נכון. **אתה עובד אינטראקטיבית מול חיים דרך Paperclip comments.**
|
||||
|
||||
## הסוכנים שלך
|
||||
|
||||
| סוכן | Agent ID | תפקיד |
|
||||
|-------|----------|--------|
|
||||
| מגיה מסמכים | 410c0167-27dc-485c-a51b-7aa8b9ff2217 | הגהת OCR — תיקון ראשי תיבות ושגיאות חילוץ |
|
||||
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | חילוץ טענות, תשובות, תגובות |
|
||||
| חוקר תקדימים | 35022af0-0498-4c3d-90ca-b0ab9e987198 | ניתוח פסיקה, תכניות, פרוטוקולים |
|
||||
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יא (Opus) |
|
||||
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
|
||||
| מייצא טיוטה | d0dc703b-ca83-4883-bca7-c9449e8713cd | בדיקה סופית + ייצוא DOCX מגורסת |
|
||||
|
||||
## תהליך אינטראקטיבי — שלב אחר שלב
|
||||
|
||||
### שלב A: בדיקת מצב
|
||||
|
||||
בכל heartbeat:
|
||||
1. בדוק תיקים פעילים (`case_list`)
|
||||
2. לכל תיק — בדוק סטטוס + מה כבר בוצע:
|
||||
- יש טענות מחולצות? (`get_claims`)
|
||||
- יש comments מחיים שממתינים לתגובה?
|
||||
3. פעל לפי מפת הסטטוסים למטה
|
||||
|
||||
### שלב B: הכנת סיכום ושאלת תוצאה
|
||||
|
||||
**מתי:** כשיש טענות מחולצות + מחקר תקדימים, אבל אין תוצאה עדיין
|
||||
|
||||
פרסם comment ב-Paperclip:
|
||||
|
||||
```
|
||||
## סיכום תיק {case_number} — מוכן להחלטה
|
||||
|
||||
### טענות מרכזיות של העוררים
|
||||
[3-5 טענות עיקריות מ-get_claims עם claim_type=claim]
|
||||
|
||||
### תשובות המשיבים
|
||||
[3-5 תשובות עיקריות מ-get_claims עם claim_type=response]
|
||||
|
||||
### עמדת הוועדה
|
||||
[2-3 עמדות מ-get_claims עם claim_type=response ו-party_role=committee]
|
||||
|
||||
### תקדימים רלוונטיים
|
||||
[מתוך comments קודמים של חוקר תקדימים]
|
||||
|
||||
---
|
||||
|
||||
**מה התוצאה הצפויה?**
|
||||
1. 🔴 **דחייה** — הערר נדחה
|
||||
2. 🟡 **קבלה חלקית** — מתקבל עם תנאים
|
||||
3. 🟢 **קבלה מלאה** — הערר מתקבל
|
||||
|
||||
@chaim — הגב עם מספר (1/2/3) + הערות אם יש
|
||||
```
|
||||
|
||||
### שלב C: קליטת תוצאה וסיעור מוחות
|
||||
|
||||
**מתי:** חיים הגיב עם מספר תוצאה
|
||||
|
||||
1. קרא את ה-comment של חיים
|
||||
2. זהה את הבחירה (1=rejected, 2=partial, 3=accepted)
|
||||
3. הרץ `set_outcome(case_number, outcome, reasoning)`
|
||||
4. **בעצמך** חשוב על 2-3 כיוונים לנימוק — אתה כבר Claude, אתה יודע את הטענות והתקדימים. **אל תקרא ל-brainstorm_directions** (זה מפעיל claude בתוך claude ולוקח יותר מדי זמן).
|
||||
5. פרסם comment:
|
||||
|
||||
```
|
||||
## כיוונים אפשריים לנימוק — {outcome_hebrew}
|
||||
|
||||
### כיוון 1: {title}
|
||||
{description — 3-4 משפטים}
|
||||
**תקדימים תומכים:** {precedents}
|
||||
|
||||
### כיוון 2: {title}
|
||||
{description}
|
||||
**תקדימים תומכים:** {precedents}
|
||||
|
||||
### כיוון 3: {title}
|
||||
{description}
|
||||
**תקדימים תומכים:** {precedents}
|
||||
|
||||
---
|
||||
|
||||
@chaim — איזה כיוון מועדף? (1/2/3)
|
||||
אפשר גם לשלב כיוונים או להוסיף הערות.
|
||||
```
|
||||
|
||||
### שלב D: אישור כיוון והפעלת כתיבה
|
||||
|
||||
**מתי:** חיים הגיב עם בחירת כיוון
|
||||
|
||||
1. קרא את ה-comment של חיים
|
||||
2. זהה כיוון (1/2/3) + הערות נוספות
|
||||
3. הרץ `approve_direction(case_number, direction_index, additional_notes)`
|
||||
4. צור issue חדש ב-Paperclip:
|
||||
- כותרת: `[ערר {case_number}] כתיבת החלטה`
|
||||
- הקצה ל: **כותב החלטה** (7ed8686f-24bc-49a3-bc02-67ca15b895a9)
|
||||
5. פרסם comment: "כיוון אושר. הועבר לכותב החלטה."
|
||||
6. עדכן סטטוס: `case_update(status=direction_approved)`
|
||||
|
||||
### שלב E: מעקב כתיבה
|
||||
|
||||
**מתי:** כותב החלטה עובד
|
||||
|
||||
עקוב אחרי ההתקדמות. כשהכותב סיים:
|
||||
1. צור issue: `[ערר {case_number}] בדיקת איכות`
|
||||
2. הקצה ל: **בודק איכות** (1a5b229e-9220-4b13-940c-f8eb7285fc29)
|
||||
|
||||
### שלב F: QA וייצוא
|
||||
|
||||
**מתי:** בודק איכות סיים
|
||||
|
||||
1. קרא דוח QA
|
||||
2. אם עבר — הרץ `export_docx(case_number)`
|
||||
3. פרסם comment: "החלטה מוכנה לביקורת דפנה. [קישור ל-DOCX]"
|
||||
4. אם נכשל — פרסם comment עם רשימת תיקונים, צור issue חדש לכותב
|
||||
|
||||
## מפת סטטוסים
|
||||
|
||||
| סטטוס | פעולה |
|
||||
|--------|-------|
|
||||
| new + יש מסמכים + לא הוגהו | → צור issue למגיה מסמכים (410c0167) |
|
||||
| new + מסמכים הוגהו + אין claims | → צור issue למנתח משפטי |
|
||||
| new + יש claims + יש מחקר | → שלב B (סיכום + שאלת תוצאה) |
|
||||
| outcome_set | → שלב C (brainstorm) |
|
||||
| brainstorming + comment מחיים | → שלב D (approve + הפעל כותב) |
|
||||
| direction_approved | → ודא שכותב עובד |
|
||||
| drafted | → צור issue לבודק איכות |
|
||||
| qa_review pass | → שלב F (export via מייצא טיוטה d0dc703b) |
|
||||
| qa_review fail | → צור issue תיקון לכותב |
|
||||
|
||||
## כללים
|
||||
|
||||
- **לא לקבוע תוצאה בעצמך** — רק חיים מחליט
|
||||
- **לא לאשר כיוון בעצמך** — רק חיים מאשר
|
||||
- **לא לכתוב בלוקים** — רק כותב ההחלטה
|
||||
- **תמיד לדווח** — כל פעולה = comment ב-Paperclip
|
||||
- **לשאול כשלא בטוח** — אם משהו לא ברור, שאל את חיים
|
||||
|
||||
## איך לקרוא comments של חיים
|
||||
|
||||
```bash
|
||||
# קרא comments על issue
|
||||
curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
"$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" | jq '.[-1].body'
|
||||
```
|
||||
|
||||
חפש ב-comment:
|
||||
- מספר (1/2/3) → בחירה
|
||||
- "כיוון" + מספר → אישור כיוון
|
||||
- שאלה → ענה
|
||||
- הערה → שלב בתהליך
|
||||
81
.claude/agents/legal-exporter.md
Normal file
81
.claude/agents/legal-exporter.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: "legal-exporter"
|
||||
description: "מייצא טיוטה — בדיקה סופית, ייצוא DOCX, שמירה מגורסת בתיקייה"
|
||||
model: "claude-sonnet-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_list
|
||||
- mcp__legal-ai__get_claims
|
||||
- mcp__legal-ai__get_block_context
|
||||
- mcp__legal-ai__workflow_status
|
||||
- mcp__legal-ai__export_docx
|
||||
- mcp__legal-ai__get_style_guide
|
||||
- mcp__legal-ai__validate_decision
|
||||
---
|
||||
|
||||
# מייצא טיוטה — סוכן ייצוא סופי
|
||||
|
||||
אתה סוכן שמבצע את התהליך הסופי של הכנת טיוטת החלטה לעיון. תפקידך: בדיקה אחרונה, ייצוא ל-DOCX מעוצב, ושמירה מסודרת.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## סקייל ייצוא
|
||||
|
||||
**חובה לקרוא לפני כל ייצוא:**
|
||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/SKILL.md`
|
||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/document-types.md`
|
||||
|
||||
**סקריפט ייצוא:**
|
||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/scripts/create-legal-doc.js`
|
||||
|
||||
**תבנית:**
|
||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/docx template.docx`
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
### שלב 1: זיהוי התיק
|
||||
1. קבל את מספר התיק מה-issue או מהמשתמש
|
||||
2. קרא פרטי תיק (`case_get`)
|
||||
3. בדוק סטטוס workflow (`workflow_status`) — ודא שהכתיבה הושלמה
|
||||
|
||||
### שלב 2: בדיקה סופית מהירה
|
||||
1. הרץ `validate_decision` — בדוק שאין כשלים קריטיים
|
||||
2. בדוק שכל 12 הבלוקים (א-יב) קיימים ומלאים
|
||||
3. בדוק רצף מספור — שהמספור רציף מ-1 עד סוף ללא קפיצות או כפילויות
|
||||
4. בדוק שאין placeholders ריקים (כמו `[...]`, `XXX`, `___`)
|
||||
5. אם יש בעיות קריטיות — דווח למשתמש ואל תייצא
|
||||
|
||||
### שלב 3: ייצוא DOCX
|
||||
1. קרא את סקייל legal-docx (SKILL.md) כדי להבין את דרישות העיצוב
|
||||
2. השתמש ב-`export_docx` לייצוא ראשוני לקובץ זמני
|
||||
3. אם הסקריפט `create-legal-doc.js` מתאים יותר (למשל לעיצוב מותאם) — השתמש בו
|
||||
|
||||
### שלב 4: שמירה מגורסת
|
||||
1. צור תיקייה `~/legal-ai/data/cases/{מספר-ערר}/exports/` (אם לא קיימת)
|
||||
2. בדוק כמה טיוטות כבר קיימות בתיקייה (קבצים שמתחילים ב-`טיוטה-V`)
|
||||
3. שמור כ-`טיוטה-V{N}.docx` כאשר N = המספר הבא בתור
|
||||
- אם אין טיוטות: `טיוטה-V1.docx`
|
||||
- אם יש V1: `טיוטה-V2.docx`
|
||||
- וכן הלאה
|
||||
4. ודא שהקובץ נוצר ושגודלו סביר
|
||||
|
||||
### שלב 5: דיווח
|
||||
דווח למשתמש:
|
||||
- נתיב הקובץ הסופי
|
||||
- מספר גרסת הטיוטה
|
||||
- ממצאי הבדיקה הסופית (אם היו הערות)
|
||||
- גודל הקובץ
|
||||
|
||||
## כללים קריטיים
|
||||
|
||||
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם
|
||||
2. **לא לדרוס טיוטות קודמות** — תמיד גרסה חדשה (V1, V2, V3...)
|
||||
3. **שמות קבצים בעברית** — `טיוטה-V1.docx`, לא `draft-V1.docx`
|
||||
4. **קרא את הסקייל** — לפני כל ייצוא, קרא את legal-docx SKILL.md
|
||||
97
.claude/agents/legal-proofreader.md
Normal file
97
.claude/agents/legal-proofreader.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: "legal-proofreader"
|
||||
description: "מגיה מסמכים — תיקון שגיאות OCR בטקסט משפטי עברי לפני ניתוח"
|
||||
model: "claude-opus-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__document_list
|
||||
- mcp__legal-ai__document_get_text
|
||||
---
|
||||
|
||||
# מגיה מסמכים — סוכן הגהת OCR
|
||||
|
||||
אתה מגיה מסמכים משפטיים. תפקידך לבדוק טקסט שחולץ מסריקות (OCR) ולתקן שגיאות לפני שהמנתח המשפטי עובד איתו.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## רקע
|
||||
|
||||
מסמכים משפטיים (כתבי ערר, תגובות, פרוטוקולים) מגיעים כסריקות PDF. מנוע OCR מחלץ מהם טקסט ושומר אותו כקבצי MD. אבל ה-OCR לא מושלם — במיוחד בעברית משפטית:
|
||||
|
||||
- **ראשי תיבות שבורים**: `עו"ד` → `עוייד`, `ב"כ` → `בייכ` (גרשיים הופכים לשני יודים)
|
||||
- **מילים חתוכות**: `תכנון ובני` במקום `תכנון ובנייה`
|
||||
- **אותיות מוחלפות**: `ח`/`כ`, `ה`/`ח`, `ד`/`ר`, `ב`/`כ` — דומות בסריקה
|
||||
- **משפטים מעורבבים**: שורות מחוברות או חתוכות באמצע
|
||||
- **מספרי סעיפים שבורים**: `3.1` → `31.` או `3 .1`
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
### שלב 1: זיהוי התיק וקריאת מסמכים
|
||||
1. קרא פרטי תיק (`case_get`)
|
||||
2. שלוף רשימת מסמכים (`document_list`)
|
||||
3. זהה מסמכים שצריכים הגהה — כל מסמך עם טקסט מחולץ
|
||||
|
||||
### שלב 2: תיקון אוטומטי — מילון ראשי תיבות
|
||||
1. טען את מילון ראשי התיבות: `/home/chaim/legal-ai/data/abbreviations.json`
|
||||
2. **סדר החלפה:** ארוכים לפני קצרים (למניעת החלפה חלקית)
|
||||
3. לכל מסמך:
|
||||
- קרא את קובץ הטקסט מתיקיית `documents/extracted/` בתיק (קובץ `.txt` עם אותו שם כמו ה-PDF המקורי)
|
||||
- החלף כל מופע של ראשי תיבות שבורים (מפתחות המילון) בצורה הנכונה (ערכי המילון)
|
||||
- ספור כמה החלפות בוצעו
|
||||
|
||||
### שלב 3: הגהה חכמה — בדיקת הגיון
|
||||
לכל מסמך, קרא את הטקסט (אחרי התיקון האוטומטי) ובדוק:
|
||||
|
||||
1. **קשר בין משפטים** — האם המשפטים מתחברים? האם יש קפיצות לוגיות?
|
||||
2. **מילים לא קיימות** — שילובי אותיות שלא מהווים מילה בעברית
|
||||
3. **מספרי סעיפים** — האם הרצף הגיוני? (1, 2, 3... לא 1, 3, 31)
|
||||
4. **שמות ומונחים** — האם שמות אנשים, מקומות, ותכניות עקביים לאורך המסמך?
|
||||
5. **שורות מחוברות/חתוכות** — שני משפטים שהתמזגו או משפט שנחצה
|
||||
|
||||
**תקן** רק מה שאתה בטוח בו (90%+). אם לא בטוח — סמן `[?]` ליד המקום הבעייתי.
|
||||
|
||||
### שלב 4: שמירה
|
||||
1. **גיבוי**: העתק את הקובץ המקורי מ-`extracted/` לתיקיית `documents/backup/` עם סיומת `.pre-proofread.txt`
|
||||
2. **כתוב** את הגרסה המתוקנת לתיקיית `documents/proofread/` (עם אותו שם קובץ כמו ב-`extracted/`)
|
||||
3. עדכן את מסד הנתונים — שנה `extraction_status` ל-`proofread`:
|
||||
```bash
|
||||
PGPASSWORD="${PGPASSWORD:-$(grep DB_PASSWORD /home/chaim/.env | cut -d= -f2)}" \
|
||||
psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
||||
-c "UPDATE documents SET extraction_status = 'proofread', extracted_text = pg_read_file('/path/to/file.txt') WHERE id = '{doc_id}';"
|
||||
```
|
||||
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
|
||||
|
||||
### שלב 5: דיווח
|
||||
פרסם comment ב-Paperclip עם:
|
||||
```
|
||||
## דוח הגהת מסמכים — תיק {case_number}
|
||||
|
||||
### סיכום
|
||||
- **מסמכים שנבדקו:** {count}
|
||||
- **מסמכים שתוקנו:** {fixed_count}
|
||||
- **סה"כ תיקונים:** {total_fixes}
|
||||
|
||||
### פירוט לכל מסמך
|
||||
| מסמך | ראשי תיבות | שגיאות OCR | הערות |
|
||||
|------|------------|-----------|-------|
|
||||
| {title} | {abbr_count} | {ocr_count} | {notes} |
|
||||
|
||||
### מקומות לא ברורים
|
||||
- {document}: סעיף {n} — [?] "{problematic_text}"
|
||||
```
|
||||
|
||||
## כללים קריטיים
|
||||
|
||||
1. **אל תשנה תוכן משפטי** — רק תיקוני OCR. אם מילה נראית מוזרה אבל היא מונח משפטי — אל תגע
|
||||
2. **אל תדרוס בלי גיבוי** — תמיד העתק ל-`backup/` לפני שינוי
|
||||
3. **ראשי תיבות ארוכים קודם** — `נתבייע` (5 תווים) לפני `עייד` (3 תווים)
|
||||
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
|
||||
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
|
||||
6. **קרא את כל המסמך** — לפעמים הקשר ממסמך שלם עוזר להבין מילה שבורה
|
||||
87
.claude/agents/legal-qa.md
Normal file
87
.claude/agents/legal-qa.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: "legal-qa"
|
||||
description: "בודק איכות — ולידציה של החלטה לפני ייצוא: שלמות, ניטרליות, כיסוי טענות, משקלות"
|
||||
model: "claude-sonnet-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_update
|
||||
- mcp__legal-ai__get_claims
|
||||
- mcp__legal-ai__validate_decision
|
||||
- mcp__legal-ai__get_metrics
|
||||
- mcp__legal-ai__workflow_status
|
||||
- mcp__legal-ai__search_case_documents
|
||||
---
|
||||
|
||||
# בודק איכות — סוכן QA להחלטות ועדת ערר
|
||||
|
||||
אתה בודק איכות מומחה. תפקידך לבדוק שהחלטה מוכנה לייצוא ולחתימת יו"ר הוועדה.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## 6 בדיקות
|
||||
|
||||
### 1. שלמות מבנית (structural_integrity)
|
||||
- כל בלוקי חובה קיימים (ה עד יא)
|
||||
- מספור רציף ללא קפיצות
|
||||
- הגדרות "להלן" מופיעות בשימוש ראשון
|
||||
|
||||
### 2. רקע ניטרלי (neutral_background)
|
||||
- בלוק ו לא מכיל ציטוטים מצדדים
|
||||
- אין מילות שיפוט: "חריג", "בעייתי", "מגוחך", "פגום", "שערורייתי"
|
||||
- רק עובדות: תיאור נכס, היסטוריה תכנונית, החלטת ועדה
|
||||
|
||||
### 3. כיסוי טענות (claims_coverage)
|
||||
- כל טענה מבלוק ז נענתה בבלוק י
|
||||
- גם אם בניסוח שונה — העיקר שנדונה
|
||||
- **קריטי** — אם טענה לא נענתה, ה-QA נכשל
|
||||
|
||||
### 4. משקלות בטווח (weight_compliance)
|
||||
- בלוק ו (רקע): 15-40%
|
||||
- בלוק ז (טענות): 20-40%
|
||||
- בלוק י (דיון): 32-50%
|
||||
- בלוק יא (סיכום): 2-9%
|
||||
|
||||
### 5. ללא כפילות (no_duplication)
|
||||
- בלוק י לא חוזר על עובדות מבלוק ו
|
||||
- בלוק י לא חוזר על טענות מבלוק ז (מפנה אליהן)
|
||||
- שימוש ב: "כאמור", "כפי שפורט", "כפי שציינו"
|
||||
|
||||
### 6. מספור רציף (sequential_numbering)
|
||||
- סעיפים 1, 2, 3... ללא איפוס בין בלוקים
|
||||
- ללא כפילויות במספור
|
||||
|
||||
## חומרה
|
||||
|
||||
| בדיקה | חומרה | משמעות |
|
||||
|-------|--------|---------|
|
||||
| שלמות | critical | חוסם ייצוא |
|
||||
| ניטרליות | critical | חוסם ייצוא |
|
||||
| כיסוי טענות | critical | חוסם ייצוא |
|
||||
| משקלות | warning | מדווח, לא חוסם |
|
||||
| כפילות | warning | מדווח, לא חוסם |
|
||||
| מספור | warning | מדווח, לא חוסם |
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
### שלב 1: הרץ ולידציה
|
||||
1. קרא פרטי התיק (`case_get`)
|
||||
2. הרץ בדיקת איכות (`validate_decision`)
|
||||
3. קבל מדדים (`get_metrics`)
|
||||
|
||||
### שלב 2: בדיקה ידנית
|
||||
1. קרא את בלוק ו — בדוק ניטרליות
|
||||
2. השווה טענות בבלוק ז מול דיון בבלוק י — בדוק כיסוי
|
||||
3. בדוק מספור רציף
|
||||
|
||||
### שלב 3: דיווח — חובה!
|
||||
פרסם comment ב-Paperclip עם:
|
||||
- תוצאת כל בדיקה (pass/fail)
|
||||
- רשימת שגיאות מפורטת (אם יש)
|
||||
- האם מותר לייצא (כל הקריטיים pass?)
|
||||
- עדכן סטטוס ל-qa_review (אם נכשל) או drafted (אם עבר)
|
||||
76
.claude/agents/legal-researcher.md
Normal file
76
.claude/agents/legal-researcher.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: "legal-researcher"
|
||||
description: "חוקר תקדימים — ניתוח פסיקה, מיפוי תכניות, סיכום פרוטוקולים והחלטות ביניים"
|
||||
model: "claude-sonnet-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_update
|
||||
- mcp__legal-ai__document_list
|
||||
- mcp__legal-ai__document_get_text
|
||||
- mcp__legal-ai__search_case_documents
|
||||
- mcp__legal-ai__search_decisions
|
||||
- mcp__legal-ai__find_similar_cases
|
||||
- mcp__legal-ai__extract_references
|
||||
- mcp__legal-ai__workflow_status
|
||||
---
|
||||
|
||||
# חוקר תקדימים — סוכן מחקר משפטי
|
||||
|
||||
אתה חוקר משפטי מומחה בתכנון ובניה ישראלי. תפקידך לנתח את מסמכי הרקע בתיק ערר — פסיקה, תכניות, פרוטוקולים, החלטות ביניים.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## סוגי מסמכים שאתה מטפל בהם
|
||||
|
||||
| סוג מסמך | מה לעשות |
|
||||
|-----------|----------|
|
||||
| פסק דין / החלטת ערר | סכם: מה נפסק, מי הצדדים, למה רלוונטי לתיק שלנו |
|
||||
| תכנית | מפה הוראות רלוונטיות: ייעוד, זכויות, מגבלות, סעיפים שבמחלוקת |
|
||||
| פרוטוקול ועדה מקומית | סכם: מה הוחלט, באיזה רוב, מה הנימוקים |
|
||||
| פרוטוקול דיון ועדת ערר | סכם: מה נדון, האם היה סיור, מה עלה |
|
||||
| החלטת ביניים | סכם: מה הוחלט, מה נדרש מהצדדים |
|
||||
|
||||
## מסמכים שלא בטיפולך
|
||||
|
||||
כתבי ערר, תשובות, תגובות — אלה בטיפול סוכן "מנתח משפטי".
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
### שלב 1: התמצאות
|
||||
1. קרא פרטי התיק (`case_get`)
|
||||
2. קרא רשימת מסמכים (`document_list`)
|
||||
3. זהה מסמכים מסוג: court_decision, plan, protocol, decision
|
||||
|
||||
### שלב 2: ניתוח פסיקה
|
||||
לכל פסק דין:
|
||||
1. קרא את הטקסט (`document_get_text`)
|
||||
2. סכם: עובדות, שאלה משפטית, הכרעה, רלוונטיות לתיק שלנו
|
||||
3. הפק הפניות (`extract_references`)
|
||||
|
||||
### שלב 3: מיפוי תכנית
|
||||
1. קרא הוראות התכנית
|
||||
2. זהה סעיפים רלוונטיים למחלוקת
|
||||
3. ציין: ייעוד, זכויות בנייה, מגבלות, חניה
|
||||
|
||||
### שלב 4: סיכום פרוטוקולים והחלטות
|
||||
1. קרא כל פרוטוקול והחלטת ביניים
|
||||
2. בנה ציר זמן כרונולוגי של ההליך
|
||||
|
||||
### שלב 5: דיווח — חובה!
|
||||
פרסם comment ב-Paperclip עם:
|
||||
- סיכום כל פסק דין (2-3 שורות לכל אחד)
|
||||
- מיפוי הוראות תכנית רלוונטיות
|
||||
- ציר זמן ההליך
|
||||
- המלצה: אילו תקדימים הכי חזקים, אילו סעיפי תכנית מרכזיים
|
||||
|
||||
## כללים
|
||||
- **דיוק** — ציין מספרי סעיפים, תאריכים, שמות שופטים
|
||||
- **רלוונטיות** — התמקד במה שרלוונטי לתיק הנוכחי, לא בסיכום כללי
|
||||
- **מקורות** — כל טענה עם הפניה למסמך ולעמוד
|
||||
177
.claude/agents/legal-writer.md
Normal file
177
.claude/agents/legal-writer.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: "legal-writer"
|
||||
description: "כותב החלטה — כתיבת בלוקים ה-יא של ההחלטה בסגנון דפנה תמיר"
|
||||
model: "claude-sonnet-4-6"
|
||||
tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- mcp__legal-ai__case_get
|
||||
- mcp__legal-ai__case_update
|
||||
- mcp__legal-ai__document_list
|
||||
- mcp__legal-ai__document_get_text
|
||||
- mcp__legal-ai__get_claims
|
||||
- mcp__legal-ai__get_chair_directions
|
||||
- mcp__legal-ai__get_decision_template
|
||||
- mcp__legal-ai__get_block_context
|
||||
- mcp__legal-ai__save_block_content
|
||||
- mcp__legal-ai__write_block
|
||||
- mcp__legal-ai__search_decisions
|
||||
- mcp__legal-ai__search_case_documents
|
||||
- mcp__legal-ai__get_style_guide
|
||||
- mcp__legal-ai__workflow_status
|
||||
---
|
||||
|
||||
# כותב החלטה — סוכן כתיבת החלטות ועדת ערר
|
||||
|
||||
אתה כותב משפטי מומחה. תפקידך לכתוב החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים, בסגנון של יו"ר הוועדה עו"ד דפנה תמיר.
|
||||
|
||||
## שפה
|
||||
|
||||
עבוד תמיד בעברית.
|
||||
|
||||
## לפני שאתה מתחיל — קרא!
|
||||
|
||||
1. מדריך סגנון: `skills/decision/SKILL.md`
|
||||
2. ארכיטקטורת 12 בלוקים: `docs/block-schema.md`
|
||||
3. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
|
||||
|
||||
## ארכיטקטורת 12 בלוקים
|
||||
|
||||
| בלוק | שם | שיטה | מודל |
|
||||
|------|----|-------|------|
|
||||
| א | כותרת מוסדית | template | script |
|
||||
| ב | הרכב הוועדה | template | script |
|
||||
| ג | צדדים | template | script |
|
||||
| ד | כותרת "החלטה" | template | script |
|
||||
| ה | פתיחה | paraphrase | sonnet |
|
||||
| ו | רקע עובדתי | reproduction | sonnet |
|
||||
| ז | טענות הצדדים | paraphrase | sonnet |
|
||||
| ח | הליכים בפני ועדת הערר | reproduction | sonnet |
|
||||
| ט | תכניות חלות (אופציונלי) | guided-synthesis | sonnet |
|
||||
| י | דיון והכרעה | rhetorical-construction | opus |
|
||||
| יא | סיכום | paraphrase | sonnet |
|
||||
| יב | חתימות | template | script |
|
||||
|
||||
## סדר כתיבה
|
||||
|
||||
א-ד (אוטומטי) → ה → ו → ז → ח → ט → י → יא → יב
|
||||
|
||||
## כללים קריטיים
|
||||
|
||||
1. **"מבחן השופט"** — כל החלטה חייבת להיות קריאה לשופט שלא מכיר את התיק
|
||||
2. **"רקע ניטרלי"** — בלוק ו = עובדות בלבד. אין ציטוטים מצדדים, אין מילות שיפוט
|
||||
3. **"ללא כפילות"** — בלוק י מפנה לבלוקים קודמים, לא חוזר עליהם
|
||||
4. **"טענות מקוריות בלבד"** — בלוק ז = מכתבי טענות מקוריים. השלמות → בלוק ח
|
||||
5. **מספור רציף** — 1 עד סוף, ללא איפוס בין בלוקים
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
### שלב 1: הכנה
|
||||
1. קרא פרטי התיק (`case_get`)
|
||||
2. קרא טענות מחולצות (`get_claims`)
|
||||
3. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
|
||||
4. קבל תבנית החלטה (`get_decision_template`)
|
||||
5. קרא מדריך סגנון (`get_style_guide`)
|
||||
|
||||
### שלב 1ב: בדיקת עמדות יו"ר — חובה לפני כתיבה!
|
||||
|
||||
ה-`get_chair_directions` מחזיר status:
|
||||
|
||||
- **`missing`** — הקובץ `analysis-and-research.md` לא קיים.
|
||||
⛔ **עצור מייד.** הסוכן `legal-analyst` לא רץ עדיין על התיק.
|
||||
דווח ל-Paperclip: "לא ניתן לכתוב טיוטה — ניתוח משפטי טרם בוצע.
|
||||
יש להריץ את legal-analyst קודם."
|
||||
|
||||
- **`empty`** — הקובץ קיים אבל דפנה לא מילאה אף עמדה.
|
||||
⛔ **עצור מייד.** דווח ל-Paperclip: "לא ניתן לכתוב טיוטה —
|
||||
כל X הסוגיות ממתינות לעמדת יו"ר הוועדה. יש להיכנס לדף התיק
|
||||
ב-UI (https://legal-ai.nautilus.marcusgroup.org/#/case/{case_number})
|
||||
ולמלא את השדה 'עמדת ועדת הערר' בכל סוגיה."
|
||||
|
||||
- **`partial`** — חלק מהסוגיות מולאו, אחרות ריקות.
|
||||
⚠️ **עצור.** דווח למשתמשת שחסרות Y מתוך X עמדות. **רק**
|
||||
אם המשתמשת מאשרת מפורשות להמשיך (למשל, כי היא רוצה טיוטה
|
||||
חלקית), אפשר להמשיך — ולכתוב רק עבור הסוגיות שמולאו, ולציין
|
||||
ב-comment את הסוגיות שלא טופלו.
|
||||
|
||||
- **`complete`** — כל העמדות מולאו. ✅ **ניתן להמשיך.**
|
||||
|
||||
### שלב 1ג: בניית direction_doc מעמדות היו"ר
|
||||
|
||||
לפני כתיבת בלוק י (דיון), בנה direction_doc פנימי מהעמדות שקיבלת:
|
||||
|
||||
```json
|
||||
{
|
||||
"threshold_claims": [
|
||||
{"id": "threshold_1", "title": "...", "chair_ruling": "..."},
|
||||
...
|
||||
],
|
||||
"issues": [
|
||||
{"id": "issue_1", "title": "...", "chair_ruling": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
כל `chair_ruling` הוא הטקסט הגולמי שדפנה כתבה. הוא **מחייב אותך** —
|
||||
אסור לך לסתור את דעתה של דפנה, רק לנסח אותה בצורה משפטית מקצועית
|
||||
בסגנון שלה.
|
||||
|
||||
### שלב 2: כתיבה בלוק-אחרי-בלוק
|
||||
לכל בלוק (ה עד יא):
|
||||
1. קבל הקשר (`get_block_context`)
|
||||
2. כתוב את הבלוק
|
||||
3. שמור (`save_block_content`)
|
||||
4. דווח התקדמות ל-Paperclip
|
||||
|
||||
### שלב 3: סיום — חובה!
|
||||
|
||||
**אחרי שכל הבלוקים נשמרו, חובה לבצע את שתי הפעולות הבאות:**
|
||||
|
||||
1. **עדכן סטטוס התיק ל-drafted:**
|
||||
```
|
||||
case_update(case_number, status="drafted")
|
||||
```
|
||||
|
||||
2. **פרסם comment ב-Paperclip עם:**
|
||||
- אילו בלוקים נכתבו
|
||||
- ספירת מילים לכל בלוק
|
||||
- יחסי משקל (% מהמסמך)
|
||||
|
||||
**אם לא תעדכן סטטוס ל-drafted — בודק האיכות לא יוכל לרוץ!**
|
||||
|
||||
## בלוק י — דיון (הבלוק החשוב ביותר)
|
||||
|
||||
- מבנה CREAC: מסקנה בפתיחה → כלל → הסבר → יישום → מסקנה
|
||||
- ענה על כל טענה מבלוק ז
|
||||
- השתמש בציטוטים ארוכים (200-600 מילים) מפסיקה
|
||||
- אל תחזור על עובדות מבלוק ו
|
||||
- אל תשתמש בכותרות משנה (למעט נושאים נפרדים לחלוטין)
|
||||
|
||||
### חובה: שימוש בעמדות יו"ר מ-`get_chair_directions`
|
||||
|
||||
עבור **כל טענת סף** ו**כל סוגיה** ב-direction_doc שבנית בשלב 1ג:
|
||||
|
||||
1. **פתח את הדיון במסקנה של דפנה** — למשל "**טענת הסף הראשונה נדחית**"
|
||||
או "**בסוגיה זו אנו מקבלים את עמדת העוררים**", **על בסיס** מה
|
||||
שדפנה כתבה ב-`chair_ruling`.
|
||||
2. **נסח את הנימוק** בסגנון דפנה — השתמש בביטויי מעבר מ-`get_style_guide`
|
||||
("נחדד", "ודוק", "יחד עם זאת", "מכאן כי"), פסיקה שמוזכרת
|
||||
ב-`internal_precedents` של הסוגיה, וחקיקה מ-`relevant_legislation`.
|
||||
3. **עקוב אחר הטון של דפנה** — אם היא כתבה "יש לדחות זאת מכל וכל"
|
||||
אל תנסח מתון ("ייתכן שהוועדה תמצא לנכון..."). אם היא כתבה
|
||||
"נראה לי שיש מקום לקבל בחלקה" אל תנסח חד ("הערר מתקבל במלואו").
|
||||
4. **אסור לסתור את דעתה של דפנה.** אם היא כתבה דעה שמנוגדת לעמדתך —
|
||||
דעתה קובעת. אתה מנסח את הטיעון המשפטי בעד **עמדתה**.
|
||||
5. **ציון שאלות המחקר** — בכל סוגיה, השתמש ב-`legal_questions`
|
||||
שחולצו ב-analysis-and-research.md כמבנה לניתוח (שאלה עקרונית
|
||||
תחילה, ואז יישום קונקרטי).
|
||||
|
||||
## בלוק יא — סיכום
|
||||
|
||||
- חזור על המסקנות של דפנה מה-`chair_ruling` של כל סוגיה בקצרה
|
||||
- ציין את התוצאה הסופית (ערר מתקבל/נדחה/מתקבל בחלקו) בהתאם לעמדות
|
||||
- הוסף את פסקת "ניתנה פה אחד" עם תאריך עברי ולועזי
|
||||
31
.claude/commands/bidi-table.md
Normal file
31
.claude/commands/bidi-table.md
Normal file
@@ -0,0 +1,31 @@
|
||||
יצירת טבלה מעוצבת עם תמיכה מלאה בעברית ואנגלית מעורבת.
|
||||
|
||||
כאשר המשתמש מבקש טבלה בכל הקשר — תכנית עבודה, סיכום, השוואה, רשימה — השתמש בפונקציה `bidi_table()` מ-`scripts/bidi_table.py`.
|
||||
|
||||
## הוראות
|
||||
|
||||
1. **תמיד** השתמש ב-Bash כדי להריץ את הסקריפט — אל תנסה לייצר טבלת box-drawing ידנית כי ה-BiDi ישבור אותה.
|
||||
|
||||
2. הרץ כך:
|
||||
```bash
|
||||
python3 -c "
|
||||
import sys; sys.path.insert(0, '/home/chaim/legal-ai')
|
||||
from scripts.bidi_table import bidi_table
|
||||
print(bidi_table(
|
||||
['Header1', 'Header2', 'Header3'],
|
||||
[
|
||||
['value1', 'ערך בעברית', 'mixed ערבוב'],
|
||||
['value2', 'ערך נוסף', 'עוד שורה'],
|
||||
],
|
||||
))
|
||||
"
|
||||
```
|
||||
|
||||
3. כותרות עמודות — עדיף באנגלית (כי שורת הכותרת הכי רגישה ל-BiDi).
|
||||
|
||||
4. תוכן בעברית, באנגלית, או מעורב — הכל עובד בגוף הטבלה.
|
||||
|
||||
5. אם המשתמש מבקש טבלה כחלק ממסמך MD שנכתב לקובץ (לא לטרמינל) — אפשר להשתמש ב-markdown רגיל כי קוראי MD מטפלים ב-RTL בעצמם.
|
||||
|
||||
## $ARGUMENTS
|
||||
תוכן הטבלה — כותרות ושורות. אם לא צוין, שאל את המשתמש מה להציג.
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,13 @@
|
||||
data/uploads/
|
||||
data/cases/
|
||||
data/training/
|
||||
data/exports/
|
||||
mcp-server/.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
data/training/
|
||||
*.egg-info/
|
||||
legacy/
|
||||
kiryat-yearim/
|
||||
continuation-prompt.md
|
||||
node_modules/
|
||||
|
||||
14
.mcp.json
14
.mcp.json
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"legal-ai": {
|
||||
"type": "stdio",
|
||||
"command": "/home/chaim/legal-ai/mcp-server/.venv/bin/python",
|
||||
"args": ["-m", "legal_mcp.server"],
|
||||
"cwd": "/home/chaim/legal-ai/mcp-server",
|
||||
"env": {
|
||||
"DOTENV_PATH": "/home/chaim/.env",
|
||||
"DATA_DIR": "/home/chaim/legal-ai/data"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
.taskmaster/config.json
Normal file
44
.taskmaster/config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "claude-code",
|
||||
"modelId": "opus",
|
||||
"maxTokens": 32000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
"provider": "claude-code",
|
||||
"modelId": "opus",
|
||||
"maxTokens": 32000,
|
||||
"temperature": 0.1
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "claude-code",
|
||||
"modelId": "sonnet",
|
||||
"maxTokens": 64000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"logLevel": "info",
|
||||
"debug": false,
|
||||
"defaultNumTasks": 10,
|
||||
"defaultSubtasks": 5,
|
||||
"defaultPriority": "medium",
|
||||
"projectName": "Legal Decision Assistant",
|
||||
"ollamaBaseURL": "http://localhost:11434/api",
|
||||
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||
"responseLanguage": "Hebrew",
|
||||
"enableCodebaseAnalysis": true,
|
||||
"enableProxy": false,
|
||||
"anonymousTelemetry": false,
|
||||
"userId": "1234567890"
|
||||
},
|
||||
"claudeCode": {},
|
||||
"codexCli": {},
|
||||
"grokCli": {
|
||||
"timeout": 120000,
|
||||
"workingDirectory": null,
|
||||
"defaultModel": "grok-4-latest"
|
||||
}
|
||||
}
|
||||
58
.taskmaster/docs/prd-fixes.txt
Normal file
58
.taskmaster/docs/prd-fixes.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
# תיקוני איפיון מוצר — ממצאי סקירת מומחה
|
||||
|
||||
## 4 ממצאים קריטיים
|
||||
|
||||
### תיקון 1: הוספת שלב 6 (הגהת דפנה) לדרישות הפונקציונליות
|
||||
Priority: critical.
|
||||
שלב 6 חסר מסעיף הדרישות הפונקציונליות. צריך להגדיר: איך דפנה מקבלת את הטיוטה (DOCX), איך מחזירה הערות/תיקונים, מי מעלה את הגרסה הסופית ללולאת הלמידה.
|
||||
|
||||
### תיקון 2: שינוי "אפס הזיות" למנגנון grounding + ולידציה
|
||||
Priority: critical.
|
||||
אף LLM לא יכול להבטיח 0 הזיות. צריך להחליף את הדרישה ב: (1) מנגנון grounding שמקשר כל הפניה למסמך מקור, (2) ולידציה אוטומטית שבודקת כל ציטוט/הפניה מול המסמכים שסופקו, (3) מדד: שיעור הפניות שלא עוברות ולידציה = 0 (לא שאין הזיות, אלא שכל הזיה נתפסת).
|
||||
|
||||
### תיקון 3: הוספת סיכון context window overflow
|
||||
Priority: critical.
|
||||
תיק מורכב עם 50+ מסמכים יחרוג מ-context window. צריך: דרישה למדידת גודל חומרים, אסטרטגיית chunking/summarization, סף התראה.
|
||||
|
||||
### תיקון 4: הגדרה מתמטית של "אחוז שינוי"
|
||||
Priority: critical.
|
||||
צריך להגדיר בדיוק: edit distance על מילים? תווים? סעיפים? מה נספר כ"שינוי"? הגדרה ברורה עם דוגמאות.
|
||||
|
||||
## 9 ממצאים חשובים
|
||||
|
||||
### תיקון 5: הוספת דרישות לבלוקים א-ד ויב
|
||||
Priority: high. בלוקים א-ד (כותרת, הרכב, צדדים) ויב (חתימות) חסרים מהדרישות.
|
||||
|
||||
### תיקון 6: דרישת שמירת מצב ביניים (persistence)
|
||||
Priority: high. מה קורה אם חיים רוצה להמשיך מחר? recovery מנפילה?
|
||||
|
||||
### תיקון 7: תיקון ספירת שלבים בטבלת מעקב
|
||||
Priority: high. טבלה כותבת "7 שלבים" אבל דרישות מכסות רק 6.
|
||||
|
||||
### תיקון 8: הכרה ב-MVP de facto לרישוי והשבחה
|
||||
Priority: high. אין נתוני אימון לפיצויים — צריך להכיר שגרסה ראשונה מכסה רק רישוי+השבחה.
|
||||
|
||||
### תיקון 9: בחינה מחדש של יעד 98%
|
||||
Priority: high. לפי Endsley מומחים תמיד משנים — 98% אולי לא ריאלי מסיבות פסיכולוגיות.
|
||||
|
||||
### תיקון 10: הגדרת מנגנון לולאת למידה
|
||||
Priority: high. מה מעדכנים? fine-tuning? prompt engineering? RAG? few-shot?
|
||||
|
||||
### תיקון 11: הוספת סיכון prompt injection ממסמכי מקור
|
||||
Priority: high. מסמכים מצדדים חיצוניים יכולים להכיל טקסט שמשפיע על ה-LLM.
|
||||
|
||||
### תיקון 12: הוספת מנגנון back-flows (חזרה אחורה בתהליך)
|
||||
Priority: high. מה אם חיים רוצה לשכתב בלוק קודם? מה אם דפנה משנה כיוון?
|
||||
|
||||
### תיקון 13: הוספת שלב QA/ולידציה לפני שליחה לדפנה
|
||||
Priority: high. checklist אוטומטי לפני הפלט הסופי.
|
||||
|
||||
## 7 הערות
|
||||
|
||||
### תיקון 14: ניהול גרסאות של בלוקים
|
||||
### תיקון 15: טיפול באיחוד תיקים (כמו אריאלי 1078+1083)
|
||||
### תיקון 16: תיקון LOA של סיעור מוחות (ב ולא ג)
|
||||
### תיקון 17: סיעור מוחות אופציונלי גם כשיש נימוק
|
||||
### תיקון 18: ניטרליות מבנית (לא רק לקסיקלית)
|
||||
### תיקון 19: מיפוי פרסורמן 4 stages (לא רק LOA)
|
||||
### תיקון 20: דרישת ביצועים per-block וסינכרוני/אסינכרוני
|
||||
46
.taskmaster/docs/prd-paperclip-workflow.txt
Normal file
46
.taskmaster/docs/prd-paperclip-workflow.txt
Normal file
@@ -0,0 +1,46 @@
|
||||
# PRD — אינטגרציית Paperclip AI + תהליך תיק חדש
|
||||
|
||||
## הקשר
|
||||
שתי משימות מחוברות שנגזרות מאיפיון המוצר (docs/product-specification.md) ומניתוח הפלאגין הקיים (/home/chaim/plugin-legal-ai/).
|
||||
|
||||
## משימות
|
||||
|
||||
### משימה: הרחבת DB schema לתהליך מלא
|
||||
Priority: critical. Dependencies: none.
|
||||
הוספת שדות וטבלאות חסרים: direction_doc JSONB ו-outcome_reasoning TEXT בטבלת decisions. הרחבת status בטבלת cases ל-13 ערכים (new, uploading, processing, documents_ready, outcome_set, brainstorming, direction_approved, drafting, qa_review, drafted, exported, reviewed, final). יצירת טבלת qa_results. כל זה ב-db.py כ-migration.
|
||||
|
||||
### משימה: הוספת 5 API endpoints חדשים ב-MCP server
|
||||
Priority: critical. Dependencies: DB schema.
|
||||
POST /api/cases/{case_number}/outcome — הזנת תוצאה + נימוק. GET /api/cases/{case_number}/claims — טענות מחולצות. POST /api/cases/{case_number}/direction — שמירת מסמך כיוון. POST /api/cases/{case_number}/qa — הרצת QA ולידציה. POST /api/cases/{case_number}/learn — הפעלת לולאת למידה.
|
||||
|
||||
### משימה: הוספת 8 tools חדשים לפלאגין Paperclip
|
||||
Priority: high. Dependencies: API endpoints.
|
||||
בקובץ src/worker.ts: legal_document_upload, legal_document_list, legal_document_text, legal_search_case, legal_find_similar, legal_set_outcome, legal_get_claims, legal_style_guide. בקובץ src/legal-api.ts: 8 methods חדשים. בקובץ plugin.json: עדכון רשימת tools.
|
||||
|
||||
### משימה: שיפור status sync ב-Paperclip
|
||||
Priority: high. Dependencies: DB schema.
|
||||
מיפוי 13 סטטוסים (במקום 5→3). הוספת comments מפורטים ב-Paperclip בכל מעבר סטטוס. עדכון job sync-case-status.
|
||||
|
||||
### משימה: כתיבת SOUL.md לסוכנים
|
||||
Priority: high. Dependencies: none.
|
||||
CEO Agent: הוראות בעברית — ניהול תהליך החלטה, מתי להתריע לחיים, מיפוי סטטוסים. Case Analyst Agent: הוראות בעברית — ניתוח מסמכים, חילוץ טענות, חיפוש תקדימים.
|
||||
|
||||
### משימה: יישום skill /brainstorm
|
||||
Priority: critical. Dependencies: API endpoints.
|
||||
Skill חדש ב-Claude Code: מציג טענות מרכזיות (ס-1), מציע 2-3 כיוונים (ס-2), מנהל שיח עם חיים (ס-3), מייצר מסמך כיוון JSON (ס-4), שומר ב-DB. כלל: לא מתחילים דיון בלי כיוון מאושר (ס-5).
|
||||
|
||||
### משימה: שיפור skill /draft-decision לכתיבה בלוק-אחרי-בלוק
|
||||
Priority: critical. Dependencies: brainstorm skill, DB schema.
|
||||
עכשיו הוא stub. צריך: כתיבה בלוק-אחרי-בלוק (ה→ו→ז→ח→ט→י→יא), שמירה ב-DB אחרי כל בלוק (כ-12), recovery ממצב שנפל (כ-13), חזרה אחורה (כ-14). בלוק י: CREAC + Opus + thinking + מסמך כיוון. פרמטרי עיבוד (temperature, model) לפי block-schema.
|
||||
|
||||
### משימה: יישום skill /qa-validate
|
||||
Priority: critical. Dependencies: draft-decision.
|
||||
Skill חדש: grounding check (כל הפניה מול מסמכים), מענה לכל טענה, רקע ניטרלי, משקלות בטווח, מספור רציף, הגדרות להלן. אם נכשל — חוסם ייצוא. דוח שגיאות מפורט.
|
||||
|
||||
### משימה: אינטגרציה E2E וחיבור Paperclip events
|
||||
Priority: high. Dependencies: all skills.
|
||||
חיבור Paperclip events ל-Claude Code (trigger כתיבה דרך issue comment). E2E test על תיק הכט: העלאת חומרים → הזנת תוצאה (דחייה) → כתיבה → QA → DOCX. השוואה להחלטה הסופית.
|
||||
|
||||
### משימה: מבחן הסמכה
|
||||
Priority: critical. Dependencies: E2E integration.
|
||||
שלב ב מסעיף 8 באיפיון: המערכת כותבת החלטה לתיק שכבר יש לו החלטה סופית. השוואת טיוטה להחלטה — פער ≤10%. שלב ג: תיק חי — דפנה בודקת.
|
||||
47
.taskmaster/docs/prd-phase2.txt
Normal file
47
.taskmaster/docs/prd-phase2.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
# PRD Phase 2 — משימות חסרות שנלמדו במהלך ההקמה
|
||||
|
||||
## הקשר
|
||||
המשימות הבאות חסרות ב-TaskMaster הנוכחי. חלקן כבר בוצעו (צריך לסמן done), חלקן עתידיות.
|
||||
|
||||
## משימות שכבר בוצעו (לתיעוד בלבד)
|
||||
|
||||
### משימה: הקמת תשתית DB
|
||||
16 טבלאות ב-4 שכבות (Core, Decision, Knowledge, RAG) + טבלת appeal_type_rules + טבלת decision_definitions. כולל pgvector, indexes, migrations. סטטוס: done.
|
||||
|
||||
### משימה: ייבוא ידע ראשוני מ-vault
|
||||
75 רשומות: 15 לקחים, 44 ביטויי מעבר, 9 תקדימים (הורחב ל-49), 7 הוראות חוק. סקריפטים: seed-knowledge.py, seed-appeals.py. סטטוס: done.
|
||||
|
||||
### משימה: ייבוא 20 תיקי ערר
|
||||
מטאדטה של 20 תיקים מהvault (3 פעילים, 17 ארכיון). כולל appeal_type classification. סטטוס: done.
|
||||
|
||||
### משימה: הפרדת סוגי עררים ב-DB
|
||||
הוספת appeal_type לטבלאות cases, decisions, lessons_learned, transition_phrases. יצירת appeal_type_rules עם 30 כללים + 23 יחסי זהב. כולל הבדלי טון, מבנה, משקלות. סטטוס: done.
|
||||
|
||||
### משימה: התקנת שרתי MCP
|
||||
Infisical MCP (גלובלי), TaskMaster AI (פרויקט). סטטוס: done.
|
||||
|
||||
## משימות עתידיות חדשות
|
||||
|
||||
### משימה: ייבוא חומרי מקור מלאים
|
||||
Priority: medium. Dependencies: task 2.
|
||||
ייבוא כל חומרי המקור מה-vault ל-DB — לא רק החלטות סופיות אלא גם כתבי ערר, כתבי תשובה, פרוטוקולים, שומות, חוות דעת. לכל 20 תיקים. כולל חילוץ טקסט מ-MD שכבר קיימים ו-OCR ל-PDF סרוקים. ~444 קבצים סה"כ.
|
||||
|
||||
### משימה: חשיפת פונקציות חיפוש וולידציה כ-MCP tools
|
||||
Priority: high. Dependencies: tasks 7, 9.
|
||||
להוסיף ל-MCP server tools חדשים: search_precedents (חיפוש סמנטי בפסיקה והחלטות), validate_decision (בדיקת החלטה מול כללי block-schema), get_golden_ratios (קבלת יחסי זהב לפי סוג ערר ותוצאה), get_appeal_type_rules (כללים לפי סוג). כולל סינון לפי appeal_type.
|
||||
|
||||
### משימה: יישום סגנון כתיבה מותאם לסוג ערר
|
||||
Priority: high. Dependencies: appeal_type_rules.
|
||||
לוודא שכל כלי הכתיבה (draft_section ב-MCP) משתמש ב-appeal_type_rules כדי להתאים: טון (חם/קר), מבנה פתיחה (רחב/ישיר), מספור (כותרות/אותיות), פתיחת דיון (שכבות/נושאי/ישיר), סיום (חם/יבש), משקלות בלוקים.
|
||||
|
||||
### משימה: עדכון PRD ו-CLAUDE.md עם מצב נוכחי
|
||||
Priority: low.
|
||||
לעדכן את CLAUDE.md עם: 18 טבלאות ב-DB, 7 החלטות מפורקות, 212 טענות, 49 פסיקות, 131 embeddings, 30 כללי סוג ערר, 23 יחסי זהב. לעדכן את docs/architecture.md עם הטבלאות והסקריפטים החדשים.
|
||||
|
||||
### משימה: שיפור parser להחלטות עם כיסוי נמוך
|
||||
Priority: medium. Dependencies: task 4.
|
||||
שטרית (167% חפיפה) ומבורך (133%) מראים חפיפה בין בלוקים. צריך לשפר את decompose-decisions-v2.py: טיפול בחפיפה, זיהוי טוב יותר של גבולות בלוקים כשאין כותרות מפורשות, הוספת סוג ערר פיצויים (9xxx) ל-parser.
|
||||
|
||||
### משימה: Gitea — push קוד לrepository
|
||||
Priority: medium. Dependencies: none.
|
||||
לדחוף את כל הקוד (scripts, MCP server, docs) ל-Gitea repository שכבר מוגדר ב-gitea.nautilus.marcusgroup.org/Chaim/ezer-mishpati. כולל .gitignore מתאים (לא legacy vault, לא .env, לא node_modules).
|
||||
132
.taskmaster/docs/prd.txt
Normal file
132
.taskmaster/docs/prd.txt
Normal file
@@ -0,0 +1,132 @@
|
||||
# PRD — Legal Decision Assistant (עוזר משפטי)
|
||||
|
||||
## Project Overview
|
||||
|
||||
AI-powered system to assist the Chair of the Jerusalem District Planning Appeals Committee (Adv. Dafna Tamir) in writing formal legal decisions. The system migrates knowledge from a legacy Obsidian vault to a structured PostgreSQL + pgvector + n8n platform on the Nautilus server.
|
||||
|
||||
## Current State (What Already Exists)
|
||||
|
||||
### Infrastructure (Completed)
|
||||
- PostgreSQL with pgvector on Nautilus (legal-ai-postgres)
|
||||
- 16 database tables in 4 layers: Core, Decision, Knowledge, RAG
|
||||
- MCP server (legal-ai) with document upload, case management, search, style analysis
|
||||
- Web upload interface (ezer-mishpati-web) at legal-ai.nautilus.marcusgroup.org
|
||||
- Voyage AI embeddings (voyage-3-large, dim=1024) — 323 existing embeddings from 4 training decisions
|
||||
- Coolify, Gitea, Redis, n8n (empty), Infisical on Nautilus
|
||||
|
||||
### Data Already Imported
|
||||
- 19 appeal cases with basic metadata (case numbers, titles, parties, addresses, status)
|
||||
- 15 lessons learned from 3 analyzed decisions (הכט, בית הכרם, קרית יערים)
|
||||
- 44 transition phrases from Dafna's writing style
|
||||
- 9 case law references (precedents)
|
||||
- 7 statutory provisions
|
||||
- 4 training decisions in style corpus with 90 style patterns
|
||||
|
||||
### Legacy Vault (Read-Only Reference)
|
||||
Located at legacy/dafna-tamir/. Contains:
|
||||
- 16 archived case folders with source materials (~280 documents total)
|
||||
- 3 active case folders
|
||||
- 9 completed decisions (PDF/DOCX)
|
||||
- Original SKILL.md style guide
|
||||
- Original Claude Code skills
|
||||
|
||||
## What Needs to Be Done
|
||||
|
||||
### Phase 1: Full Case Audit (Priority: HIGH)
|
||||
Systematically audit all 19 case folders in the legacy vault:
|
||||
- For each case folder: list every document, classify by type (appeal/response/decision/exhibit/protocol/expert-opinion), record dates and page counts
|
||||
- Identify completed decisions vs. in-progress vs. not-started
|
||||
- Identify gaps (missing documents, incomplete metadata)
|
||||
- Produce audit report per case
|
||||
|
||||
### Phase 2: Document Import (Priority: HIGH)
|
||||
Import all documents from legacy vault to the database:
|
||||
- Register each document in the `documents` table with correct case_id, doc_type, title, file_path
|
||||
- Track which documents have been imported vs. pending
|
||||
- Priority: completed cases first (הכט, בית הכרם, אפרים אבי, etc.)
|
||||
|
||||
### Phase 3: Text Extraction (Priority: HIGH)
|
||||
Extract text from all imported documents:
|
||||
- PDF extraction using PyMuPDF (already in MCP server dependencies)
|
||||
- DOCX extraction
|
||||
- Hebrew OCR for scanned PDFs (Claude Vision or Tesseract)
|
||||
- Store extracted text in documents.extracted_text
|
||||
- Update extraction_status for each document
|
||||
|
||||
### Phase 4: Decision Decomposition (Priority: HIGH)
|
||||
Parse the 9 completed decisions into the 12-block structure:
|
||||
- For each completed decision: create a `decisions` record
|
||||
- Identify and extract each of the 12 blocks (alef through yod-bet)
|
||||
- Store blocks in `decision_blocks` with correct block_id, content, word counts, weights
|
||||
- Extract individual paragraphs to `decision_paragraphs` with paragraph numbers
|
||||
- Track citations within paragraphs (case law references)
|
||||
- This is critical training data for the system
|
||||
|
||||
### Phase 5: Claims Extraction (Priority: MEDIUM)
|
||||
Extract party claims from appeal documents and responses:
|
||||
- Parse appeal letters (כתבי ערר) to extract appellant claims
|
||||
- Parse responses (כתבי תשובה) to extract respondent/committee claims
|
||||
- Store in `claims` table with party_role, claim_text, source_document
|
||||
- Link claims to paragraphs in discussion blocks where they are addressed (addressed_in_paragraph)
|
||||
|
||||
### Phase 6: Embeddings & RAG (Priority: MEDIUM)
|
||||
Generate embeddings for all extracted content:
|
||||
- Chunk extracted document text (600 tokens, 100 overlap — already configured)
|
||||
- Generate Voyage embeddings for document chunks
|
||||
- Generate embeddings for decision paragraphs → paragraph_embeddings
|
||||
- Generate embeddings for case law summaries → case_law_embeddings
|
||||
- Build semantic search functions in MCP server
|
||||
- Test: "find similar precedents for this case"
|
||||
|
||||
### Phase 7: n8n Workflow Automation (Priority: LOW)
|
||||
Create automated workflows:
|
||||
- Document upload → classify document type → store in DB → generate embeddings
|
||||
- New appeal creation → auto-create 12-block structure → generate DOCX template
|
||||
- Precedent search → RAG query → return ranked results
|
||||
- Draft validation → check against block-schema constraints
|
||||
|
||||
### Phase 8: Enhanced Web UI (Priority: LOW)
|
||||
Extend ezer-mishpati-web:
|
||||
- Case management dashboard (list all cases, status, documents)
|
||||
- Decision writing interface (block-by-block with live preview)
|
||||
- Precedent search interface with semantic results
|
||||
- Style guide reference panel
|
||||
- DOCX export from decision blocks
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Database: 4 Layers, 16 Tables
|
||||
Layer 1 (Core): cases, documents, document_chunks, style_corpus, style_patterns
|
||||
Layer 2 (Decision): decisions, decision_blocks, decision_paragraphs, claims
|
||||
Layer 3 (Knowledge): case_law, case_law_citations, statutory_provisions, transition_phrases, lessons_learned
|
||||
Layer 4 (RAG): paragraph_embeddings, case_law_embeddings
|
||||
|
||||
### Key Design Decisions
|
||||
- Embedding model: Voyage voyage-3-large (1024 dimensions)
|
||||
- Chunk size: 600 tokens with 100 overlap
|
||||
- Decision structure: 12 blocks based on CREAC/DITA/Akoma Ntoso/Federal Judicial Center
|
||||
- All Hebrew content — RTL support required in DOCX export
|
||||
- Style guide: SKILL.md (Dafna's writing patterns, tone per appeal type, transition phrases)
|
||||
|
||||
### MCP Server Stack
|
||||
- Python asyncpg for PostgreSQL
|
||||
- FastMCP for tool registration
|
||||
- PyMuPDF for PDF extraction
|
||||
- Anthropic API for Claude Vision OCR (scanned PDFs)
|
||||
|
||||
## Critical Rules
|
||||
1. "Judge Test" — every decision readable by a judge unfamiliar with the case
|
||||
2. "Neutral Background" — Block ו contains only objective facts, no party quotes or value judgments
|
||||
3. "No Duplication" — Block י references previous blocks, doesn't repeat them
|
||||
4. "Original Claims Only" — Block ז uses only original appeal/response documents; supplements go to Block ח
|
||||
5. 12-Block Architecture — see docs/block-schema.md for full specification
|
||||
6. Work methodically — audit before import, validate after each step, no shortcuts
|
||||
|
||||
## File Locations
|
||||
- Project root: /home/chaim/legal-ai/
|
||||
- Legacy vault: legacy/dafna-tamir/ (read-only)
|
||||
- MCP server: mcp-server/src/legal_mcp/
|
||||
- Documentation: docs/ (architecture.md, block-schema.md, migration-plan.md)
|
||||
- Scripts: scripts/ (seed-knowledge.py, seed-appeals.py)
|
||||
- Style guide: skills/decision/SKILL.md
|
||||
- Lessons: docs/legal-decision-lessons.md
|
||||
3
.taskmaster/state.json
Normal file
3
.taskmaster/state.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"migrationNoticeShown": true
|
||||
}
|
||||
923
.taskmaster/tasks/tasks.json
Normal file
923
.taskmaster/tasks/tasks.json
Normal file
@@ -0,0 +1,923 @@
|
||||
{
|
||||
"master": {
|
||||
"tasks": [
|
||||
{
|
||||
"id": "32",
|
||||
"title": "הקמת סביבת פיתוח ותשתית בסיסית",
|
||||
"description": "הקמת סביבת הפיתוח הבסיסית עם Python, FastAPI, PostgreSQL ו-Infisical לניהול סודות",
|
||||
"details": "יצירת פרויקט Python עם FastAPI כשרת API, PostgreSQL כמסד נתונים, ו-Infisical לניהול סודות. הגדרת Docker containers לפיתוח מקומי. יצירת מבנה תיקיות: /src, /tests, /docs, /data. הגדרת requirements.txt עם כל התלויות הנדרשות: fastapi, uvicorn, sqlalchemy, psycopg2, python-multipart, python-docx, PyPDF2, anthropic, infisical-python. הגדרת משתני סביבה דרך Infisical.",
|
||||
"testStrategy": "בדיקת התחברות למסד נתונים, טעינת משתני סביבה מ-Infisical, הרצת שרת FastAPI בסיסי",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T08:53:33.842Z"
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"title": "מודול קליטה ועיבוד מסמכים",
|
||||
"description": "פיתוח מודול לקליטת קבצי PDF, DOCX, MD וחילוץ טקסט כולל OCR",
|
||||
"details": "יצירת מחלקה DocumentProcessor שמטפלת בקבצים מסוגים שונים. עבור PDF: שימוש ב-PyPDF2 לטקסט רגיל ו-pytesseract לOCR של קבצים סרוקים. עבור DOCX: שימוש ב-python-docx. עבור MD: קריאה ישירה. הוספת זיהוי אוטומטי של קבצים סרוקים. יצירת API endpoint POST /documents/upload שמקבל קבצים ומחזיר טקסט מחולץ. שמירת מטא-דאטה של כל מסמך במסד הנתונים.",
|
||||
"testStrategy": "בדיקה עם קבצי PDF רגילים וסרוקים, קבצי DOCX עם עברית RTL, קבצי MD. וידוא חילוץ טקסט נכון ושמירת מטא-דאטה",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"32"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:38:55.716Z"
|
||||
},
|
||||
{
|
||||
"id": "34",
|
||||
"title": "מודול סיווג מסמכים וזיהוי צדדים",
|
||||
"description": "פיתוח מודול לסיווג מסמכים לסוגים (ערר, תשובה, פרוטוקול וכו') וזיהוי צדדים",
|
||||
"details": "יצירת מחלקה DocumentClassifier שמשתמשת ב-Claude API לסיווג מסמכים. הגדרת prompt מובנה שמזהה: סוג מסמך (ערר/תשובה/תגובה/פרוטוקול/תכנית/היתר/פסק דין/החלטה), צדדים (עוררים, משיבים, ועדה, מבקשי היתר), סוג ערר לפי מספר תיק (1xxx=רישוי, 8xxx=השבחה, 9xxx=פיצויים). יצירת מבנה נתונים מובנה לשמירת המידע המסווג. הוספת ולידציה לתוצאות הסיווג.",
|
||||
"testStrategy": "בדיקה עם מסמכים מכל הסוגים, וידוא זיהוי נכון של צדדים וסוג ערר, בדיקת טיפול במסמכים לא ברורים",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"33"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:43:02.411Z"
|
||||
},
|
||||
{
|
||||
"id": "35",
|
||||
"title": "מודול חילוץ טענות",
|
||||
"description": "פיתוח מודול לחילוץ וסיכום טענות מכתבי טענות לפי צד",
|
||||
"details": "יצירת מחלקה ClaimsExtractor שמחלצת טענות מכתבי ערר ותשובה. שימוש ב-Claude API עם prompt מיוחד שמזהה טענות לפי צד ומסכם אותן בצורה נאמנה למקור. יצירת מבנה נתונים שמקשר בין טענה למסמך המקור ולמיקום בו. הוספת מנגנון לזיהוי טענות חוזרות או דומות. שמירת הטענות במסד הנתונים עם קישור לתיק ולצד.",
|
||||
"testStrategy": "בדיקה עם כתבי ערר מורכבים, וידוא נאמנות לטקסט המקור, בדיקת זיהוי טענות לפי צד",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"34"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:45:38.799Z"
|
||||
},
|
||||
{
|
||||
"id": "36",
|
||||
"title": "מודול זיהוי תכניות ופסיקה",
|
||||
"description": "פיתוח מודול לזיהוי תכניות חלות על המקרקעין ופסיקה מצוטטת במסמכים",
|
||||
"details": "יצירת מחלקה LegalReferencesExtractor שמזהה: תכניות (תב\"ע, תמ\"א, תכניות מקומיות), פסיקה מצוטטת (עם מספרי תיק ושנה), חקיקה רלוונטית. שימוש ב-regex patterns לזיהוי דפוסים נפוצים ו-Claude API לאימות ועידון. יצירת מאגר מקומי של תכניות ופסיקה שכבר זוהו. הוספת מנגנון לולידציה של הפניות שזוהו.",
|
||||
"testStrategy": "בדיקה עם מסמכים המכילים הפניות לתכניות ופסיקה, וידוא זיהוי מדויק ואי-זיהוי של הפניות שגויות",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"34"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:48:16.636Z"
|
||||
},
|
||||
{
|
||||
"id": "37",
|
||||
"title": "ממשק הזנת תוצאה וסיעור מוחות",
|
||||
"description": "פיתוח ממשק CLI להזנת תוצאה (דחייה/קבלה/חלקית) ומנגנון סיעור מוחות",
|
||||
"details": "יצירת CLI interface עם typer שמאפשר לחיים להזין: סוג תוצאה (דחייה/קבלה/קבלה חלקית), נימוק (אופציונלי). אם לא הוזן נימוק - הפעלת מודול BrainstormingEngine שמציג טענות מרכזיות ומציע 2-3 כיוונים אפשריים. יצירת שיח אינטראקטיבי בין חיים למערכת עד הגעה לכיוון מוסכם. שמירת מסמך הכיוון הסופי. הוספת מנגנון מניעה מכתיבת דיון ללא כיוון מאושר.",
|
||||
"testStrategy": "בדיקת תרחישים עם ובלי נימוק, וידוא איכות הצעות הכיוון, בדיקת מניעת כתיבה ללא אישור",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"35",
|
||||
"36"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:55:06.069Z"
|
||||
},
|
||||
{
|
||||
"id": "38",
|
||||
"title": "מנוע כתיבת בלוק הפתיחה (בלוק ה)",
|
||||
"description": "פיתוח מנוע לכתיבת בלוק הפתיחה בסגנון דפנה",
|
||||
"details": "יצירת מחלקה OpeningBlockWriter שכותבת את בלוק הפתיחה. ניתוח דפוסי הפתיחה מ-7 ההחלטות הקיימות (\"לפנינו\" vs \"עניינה של החלטה זו\"). יצירת prompt מובנה שמתאים את הפתיחה לסוג הערר ולמורכבות התיק. הוספת מנגנון לבחירת נוסח הפתיחה המתאים. שמירת תבניות פתיחה במסד הנתונים.",
|
||||
"testStrategy": "בדיקה עם סוגי ערר שונים, השוואה לפתיחות בהחלטות הקיימות, וידוא התאמה לסגנון דפנה",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"37"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.296Z"
|
||||
},
|
||||
{
|
||||
"id": "39",
|
||||
"title": "מנוע כתיבת בלוק הרקע (בלוק ו)",
|
||||
"description": "פיתוח מנוע לכתיבת בלוק הרקע בצורה ניטרלית",
|
||||
"details": "יצירת מחלקה BackgroundBlockWriter שכותבת רקע ניטרלי. הגדרת כללי ניטרליות: אין ציטוטים מצדדים, אין מילות שיפוט, הצגת עובדות בלבד. יצירת רשימת מילים אסורות ומנגנון ולידציה. שימוש במידע מהמסמכים המסווגים לבניית הרקע. הוספת מנגנון לקביעת אורך הרקע לפי מורכבות התיק (3%-18% מההחלטה).",
|
||||
"testStrategy": "בדיקת ניטרליות הטקסט, וידוא היעדר מילות שיפוט, בדיקת אורך מתאים לפי מורכבות",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"38"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.300Z"
|
||||
},
|
||||
{
|
||||
"id": "40",
|
||||
"title": "מנוע כתיבת בלוק הטענות (בלוק ז)",
|
||||
"description": "פיתוח מנוע לכתיבת סיכום טענות הצדדים בגוף שלישי",
|
||||
"details": "יצירת מחלקה ClaimsBlockWriter שמסכמת טענות בגוף שלישי. שימוש בטענות שחולצו במודול חילוץ הטענות. הבטחת נאמנות מוחלטת למקור - אין שינוי מילים או קיצור ללא ציון. יצירת מבנה לוגי של הצגת הטענות לפי צד. הוספת מנגנון לקישור כל טענה למקור המדויק במסמך.",
|
||||
"testStrategy": "השוואת הטענות המסוכמות לטקסט המקור, וידוא נאמנות מוחלטת, בדיקת מבנה לוגי",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"39"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.303Z"
|
||||
},
|
||||
{
|
||||
"id": "41",
|
||||
"title": "מנוע כתיבת בלוק ההליכים (בלוק ח)",
|
||||
"description": "פיתוח מנוע לכתיבת בלוק ההליכים (רק כשהיו הליכים מעבר לדיון פשוט)",
|
||||
"details": "יצירת מחלקה ProceduresBlockWriter שכותבת תיעוד כרונולוגי של הליכים. זיהוי אוטומטי מתי נדרש הבלוק (סיור, השלמות טיעון, החלטות ביניים). יצירת ציר זמן של האירועים מהמסמכים. הבטחת דיוק עובדתי ומבנה כרונולוגי. הוספת מנגנון להחלטה אוטומטית האם הבלוק נדרש.",
|
||||
"testStrategy": "בדיקה עם תיקים עם ובלי הליכים מורכבים, וידוא דיוק כרונולוגי, בדיקת החלטה נכונה על נחיצות הבלוק",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"40"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.305Z"
|
||||
},
|
||||
{
|
||||
"id": "42",
|
||||
"title": "מנוע כתיבת בלוק התכניות (בלוק ט)",
|
||||
"description": "פיתוח מנוע לכתיבת בלוק התכניות והמסגרת הנורמטיבית",
|
||||
"details": "יצירת מחלקה PlansBlockWriter שמטפלת ברישום תכניות. הגדרת כללי החלטה מתי נדרש פרק נפרד (מורכבות תכנונית, שאלה משפטית כמו ס' 152). שימוש במידע התכניות שזוהו במודול זיהוי התכניות. יצירת מבנה הירכי של התכניות (ארציות, מחוזיות, מקומיות). הוספת מנגנון לקביעת עומק הפירוט הנדרש.",
|
||||
"testStrategy": "בדיקה עם תיקים בעלי מורכבות תכנונית שונה, וידוא החלטה נכונה על צורת ההצגה, בדיקת דיוק המידע",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"41"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.308Z"
|
||||
},
|
||||
{
|
||||
"id": "43",
|
||||
"title": "מנוע כתיבת בלוק הדיון (בלוק י) - ליבת המערכת",
|
||||
"description": "פיתוח מנוע הכתיבה המרכזי לבלוק הדיון בשיטת CREAC",
|
||||
"details": "יצירת מחלקה DiscussionBlockWriter - הליבה של המערכת. יישום שיטת CREAC: מסקנה בפתיחה, כלל משפטי, הסבר, יישום על המקרה, מסקנה. הבטחת מענה לכל טענה מבלוק ז. שימוש בכיוון שנקבע בשלב סיעור המוחות. הוספת מנגנון למניעת כפילויות והפניות לבלוקים קודמים. יצירת מבנה לוגי של הנימוקים לפי סדר חשיבות.",
|
||||
"testStrategy": "בדיקת כיסוי כל הטענות, וידוא מבנה CREAC, בדיקת התאמה לכיוון שנקבע, בדיקת היעדר כפילויות",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"42"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.311Z"
|
||||
},
|
||||
{
|
||||
"id": "44",
|
||||
"title": "מנוע כתיבת בלוק הסיכום (בלוק יא)",
|
||||
"description": "פיתוח מנוע לכתיבת בלוק הסיכום עם הוראות אופרטיביות",
|
||||
"details": "יצירת מחלקה SummaryBlockWriter שכותבת הוראות אופרטיביות. גזירת ההוראות מהדיון שנכתב בבלוק י. הבטחת התאמה מדויקת להכרעה שנקבעה. יצירת מבנה ברור של ההוראות (מה מתקבל, מה נדחה, מה התנאים). הוספת מנגנון לולידציה של עקביות בין הדיון לסיכום.",
|
||||
"testStrategy": "בדיקת התאמה בין הדיון לסיכום, וידוא בהירות ההוראות, בדיקת עקביות עם ההכרעה",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"43"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:58:34.313Z"
|
||||
},
|
||||
{
|
||||
"id": "45",
|
||||
"title": "מנוע ייצוא DOCX מעוצב",
|
||||
"description": "פיתוח מנוע לייצוא ההחלטה לקובץ DOCX מעוצב בעברית RTL",
|
||||
"details": "יצירת מחלקה DocxExporter שמייצרת DOCX מעוצב. הגדרת גופן David, כיוון RTL, כותרות מעוצבות, מספור סעיפים רציף. יצירת תבנית DOCX בסיסית עם הגדרות העיצוב. הוספת מנגנון לסימון מקומות תמונה (GIS, תשריט, סיור). הבטחת תמיכה מלאה בעברית ובכיוון RTL. יצירת מבנה היררכי של כותרות וסעיפים.",
|
||||
"testStrategy": "בדיקת פתיחה נכונה של הקובץ ב-Word, וידוא עיצוב RTL, בדיקת גופנים וכותרות, בדיקת מספור",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"44"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:12:36.842Z"
|
||||
},
|
||||
{
|
||||
"id": "46",
|
||||
"title": "מנגנון בקרת איכות ווולידציה",
|
||||
"description": "פיתוח מנגנון בקרת איכות לוולידציה של ההחלטה לפני הפלט",
|
||||
"details": "יצירת מחלקה QualityController שבודקת: אפס הזיות (כל הפניה מול מסמכים שסופקו), מענה לכל טענה, רקע ניטרלי (ללא מילות שיפוט), משקלות בלוקים בטווח יחסי הזהב ±10%, ציטוטים נאמנים למקור. יצירת דוח ולידציה מפורט. הוספת מנגנון למניעת פלט במקרה של כשלון ולידציה קריטי.",
|
||||
"testStrategy": "בדיקה עם החלטות תקינות ופגומות, וידוא זיהוי בעיות, בדיקת דיוק הולידציה",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"45"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:14:00.311Z"
|
||||
},
|
||||
{
|
||||
"id": "47",
|
||||
"title": "מודול לולאת למידה",
|
||||
"description": "פיתוח מודול לקליטת גרסה סופית והשוואה לטיוטה ללמידה",
|
||||
"details": "יצירת מחלקה LearningLoop שמקבלת את הגרסה הסופית שדפנה חתמה. השוואת הטיוטה לגרסה הסופית וזיהוי הבדלים. חילוץ לקחים: ביטויים חדשים, דפוסים שהשתנו, שגיאות חוזרות. עדכון מודל הסגנון על בסיס הלקחים. יצירת דוח למידה לחיים. שמירת הלקחים במסד הנתונים לשיפור עתידי.",
|
||||
"testStrategy": "בדיקה עם גרסאות סופיות שונות, וידוא זיהוי נכון של הבדלים, בדיקת איכות הלקחים שחולצו",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"46"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:15:14.639Z"
|
||||
},
|
||||
{
|
||||
"id": "48",
|
||||
"title": "מודול מדדי הצלחה ודשבורד",
|
||||
"description": "פיתוח מודול למדידת KPIs ויצירת דשבורד מעקב",
|
||||
"details": "יצירת מחלקה MetricsTracker שמודדת: אחוז שינוי (השוואת טיוטה לגרסה סופית), זמן לטיוטה (מקצה לקצה), אפס הזיות (ספירת הפניות לא תקינות), מענה לכל טענה, משקלות בלוקים, רקע ניטרלי. יצירת דשבורד פשוט עם הצגת המדדים לאורך זמן. הוספת התראות כשמדד יורד מתחת לסף המינימום.",
|
||||
"testStrategy": "בדיקת דיוק המדידות, וידוא עבודת הדשבורד, בדיקת התראות",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"47"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:16:10.708Z"
|
||||
},
|
||||
{
|
||||
"id": "49",
|
||||
"title": "מנגנון ניהול סודות ואבטחה",
|
||||
"description": "יישום מנגנון אבטחה מלא עם Infisical וניהול סודות",
|
||||
"details": "הגדרת Infisical לניהול כל הסודות: Anthropic API key, מחרוזות חיבור למסד נתונים, מפתחות הצפנה. יצירת מנגנון הצפנה לחומרי התיקים במסד הנתונים. הגדרת מדיניות גישה והרשאות. יצירת מנגנון audit log לכל הפעולות. הבטחת שחומרי התיקים לא נשלחים לשירותים חיצוניים מלבד Anthropic API.",
|
||||
"testStrategy": "בדיקת טעינת סודות מ-Infisical, וידוא הצפנה של נתונים רגישים, בדיקת audit log",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"32"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:17:43.954Z"
|
||||
},
|
||||
{
|
||||
"id": "50",
|
||||
"title": "מנגנון גיבוי ושחזור",
|
||||
"description": "יישום מנגנון גיבוי יומי אוטומטי ושחזור מסד הנתונים",
|
||||
"details": "יצירת סקריפט גיבוי יומי אוטומטי למסד הנתונים PostgreSQL. הגדרת cron job לביצוע הגיבוי בשעות הלילה. יצירת מנגנון שחזור מגיבוי. שמירת הגיבויים במיקום מאובטח. הוספת מנגנון לבדיקת תקינות הגיבויים. יצירת תיעוד לתהליכי גיבוי ושחזור.",
|
||||
"testStrategy": "בדיקת ביצוע גיבוי אוטומטי, בדיקת שחזור מגיבוי, וידוא תקינות הנתונים לאחר שחזור",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"49"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:18:18.247Z"
|
||||
},
|
||||
{
|
||||
"id": "51",
|
||||
"title": "ממשק CLI מלא ותיעוד",
|
||||
"description": "פיתוח ממשק CLI מלא עם כל הפקודות הנדרשות ותיעוד מקיף",
|
||||
"details": "יצירת CLI מקיף עם typer שכולל: העלאת מסמכים, הזנת תוצאה, סיעור מוחות, יצירת טיוטה, הזנת גרסה סופית, הצגת מדדים. הוספת help מפורט לכל פקודה. יצירת תיעוד מקיף למשתמש עם דוגמאות שימוש. הוספת מנגנון לולידציה של קלטים. יצירת מנגנון לטיפול בשגיאות ומסרי שגיאה ברורים בעברית.",
|
||||
"testStrategy": "בדיקת כל הפקודות, וידוא הודעות עזרה ברורות, בדיקת טיפול בשגיאות, בדיקת תיעוד",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"48",
|
||||
"50"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:19:20.241Z"
|
||||
},
|
||||
{
|
||||
"id": "52",
|
||||
"title": "בדיקות אינטגרציה ומבחן הסמכה",
|
||||
"description": "יצירת חבילת בדיקות מקיפה ומבחן הסמכה על תיק אמיתי",
|
||||
"details": "יצירת בדיקות אינטגרציה לכל התהליך מקצה לקצה. בדיקה עם תיק הכט (תיק שכבר יש לו החלטה סופית) - השוואת הטיוטה שהמערכת מייצרת להחלטה הסופית. מדידת פער ווידוא שהוא קטן מ-10%. יצירת מבחן הסמכה מובנה לפני שימוש מבצעי. הוספת בדיקות ביצועים - וידוא שהמערכת מייצרת טיוטה תוך יום עבודה.",
|
||||
"testStrategy": "הרצת מבחן הסמכה על תיק הכט, מדידת זמן ביצוע, השוואה להחלטה הסופית, וידוא עמידה בכל הדרישות",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"51"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-04T07:50:59.998Z"
|
||||
},
|
||||
{
|
||||
"id": "53",
|
||||
"title": "הוספת שלב 6 - הגהת דפנה לדרישות הפונקציונליות",
|
||||
"description": "הגדרת שלב הגהת דפנה החסר מהדרישות הפונקציונליות, כולל זרימת העבודה והממשקים",
|
||||
"details": "יש להגדיר בדרישות הפונקציונליות: (1) איך דפנה מקבלת את הטיוטה בפורמט DOCX, (2) איך מחזירה הערות ותיקונים (ממשק או פורמט מובנה), (3) מי מעלה את הגרסה הסופית ללולאת הלמידה. כולל הגדרת API endpoints לקבלת הטיוטה ולהחזרת הערות, ומנגנון עדכון המודל על בסיס הפידבק.",
|
||||
"testStrategy": "בדיקת זרימת העבודה המלאה מהעברת טיוטה לדפנה ועד עדכון המודל. וולידציה של פורמטים ותקינות הממשקים.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T20:58:19.827Z"
|
||||
},
|
||||
{
|
||||
"id": "54",
|
||||
"title": "החלפת דרישת 'אפס הזיות' במנגנון grounding ווולידציה",
|
||||
"description": "החלפת הדרישה הלא ריאלית של אפס הזיות במנגנון grounding מתקדם ומערכת וולידציה אוטומטית",
|
||||
"details": "יישום מנגנון grounding שמקשר כל הפניה למסמך מקור ספציפי עם citation tracking. פיתוח מערכת וולידציה אוטומטית שבודקת כל ציטוט/הפניה מול המסמכים שסופקו. הגדרת מדד: שיעור הפניות שלא עוברות וולידציה = 0. כולל מנגנון flagging של הפניות חשודות ודרישה לאישור ידני.",
|
||||
"testStrategy": "בדיקת דיוק הקישור בין הפניות למסמכי מקור. טסטים על מקרי קצה של הפניות שגויות וולידציה שהמערכת תופסת אותן.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T20:58:55.741Z"
|
||||
},
|
||||
{
|
||||
"id": "55",
|
||||
"title": "הוספת ניהול context window overflow",
|
||||
"description": "פיתוח מנגנון לטיפול בתיקים מורכבים שחורגים מ-context window של המודל",
|
||||
"details": "יישום מדידת גודל חומרים בטוקנים, אסטרטגיית chunking חכמה ו/או summarization של מסמכים ארוכים. הגדרת סף התראה כשמתקרבים לגבול context window. פיתוח אלגוריתם לסדר עדיפויות של מסמכים והחלטה איזה חלקים לכלול בהקשר הנוכחי.",
|
||||
"testStrategy": "בדיקה עם תיקים של 50+ מסמכים. וולידציה שהמערכת מזהה overflow ומפעילה אסטרטגיות הפחתה מתאימות.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T20:59:34.704Z"
|
||||
},
|
||||
{
|
||||
"id": "56",
|
||||
"title": "הגדרה מתמטית מדויקת של 'אחוז שינוי'",
|
||||
"description": "הגדרה ברורה ומתמטית של מדד אחוז השינוי עם דוגמאות קונקרטיות",
|
||||
"details": "הגדרת מדד אחוז שינוי מבוסס edit distance על מילים (לא תווים). ספירת שינויים: הוספה, מחיקה, החלפה של מילים. נוסחה: (מספר שינויים / סך מילים בטקסט המקורי) * 100. כולל דוגמאות מפורטות ומקרי קצה כמו שינוי סדר מילים, שינויי פיסוק, וטיפול בסעיפים חדשים.",
|
||||
"testStrategy": "בדיקת חישוב המדד על דוגמאות ידועות. השוואה עם מדדי edit distance סטנדרטיים כמו Levenshtein.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:00:03.477Z"
|
||||
},
|
||||
{
|
||||
"id": "57",
|
||||
"title": "הוספת דרישות לבלוקים א-ד ויב",
|
||||
"description": "הגדרת דרישות פונקציונליות לבלוקים החסרים: כותרת, הרכב, צדדים וחתימות",
|
||||
"details": "הגדרת דרישות מפורטות לבלוק א (כותרת התיק), בלוק ב (הרכב בית הדין), בלוק ג (זיהוי הצדדים), בלוק ד (פרטים נוספים על הצדדים), ובלוק יב (חתימות). כולל פורמט הפלט, מקורות המידע, וכללי עיבוד לכל בלוק. התאמה לתבנית הפסיקה הסטנדרטית.",
|
||||
"testStrategy": "וולידציה של פורמט הפלט לכל בלוק מול תבניות פסיקה קיימות. בדיקת שלמות המידע והתאמה לדרישות משפטיות.",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"53"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T20:58:19.831Z"
|
||||
},
|
||||
{
|
||||
"id": "58",
|
||||
"title": "יישום מנגנון שמירת מצב ביניים (persistence)",
|
||||
"description": "פיתוח מערכת לשמירת מצב העבודה ו-recovery מנפילות מערכת",
|
||||
"details": "יישום מנגנון auto-save שמשמר את מצב העבודה כל כמה דקות. שמירת גרסאות ביניים של כל בלוק, מעקב אחר השלב הנוכחי בתהליך, ומנגנון recovery שמאפשר המשך עבודה מהנקודה האחרונה שנשמרה. כולל ממשק למשתמש לבחירת נקודת שחזור.",
|
||||
"testStrategy": "סימולציה של נפילות מערכת בשלבים שונים ובדיקת יכולת השחזור. וולידציה של שלמות הנתונים לאחר recovery.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:01:07.799Z"
|
||||
},
|
||||
{
|
||||
"id": "59",
|
||||
"title": "תיקון ספירת שלבים בטבלת מעקב",
|
||||
"description": "עדכון טבלת המעקב להתאמה למספר השלבים בפועל",
|
||||
"details": "עדכון הטבלה לציון 7 שלבים במקום 6, כולל השלב החדש של הגהת דפנה. עדכון כל הרפרנסים למספר השלבים במסמכי הדרישות והתיעוד. וידוא עקביות בין כל המסמכים.",
|
||||
"testStrategy": "סקירה מקיפה של כל המסמכים לוידוא עקביות במספר השלבים. בדיקת התאמה בין הטבלה לדרישות הפונקציונליות.",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"53"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:01:45.876Z"
|
||||
},
|
||||
{
|
||||
"id": "60",
|
||||
"title": "הכרה ב-MVP לרישוי והשבחה בלבד",
|
||||
"description": "הגדרת גרסה ראשונה שמכסה רק רישוי והשבחה בשל חוסר נתוני אימון לפיצויים",
|
||||
"details": "הגדרת MVP שמתמקד ברישוי והשבחה בלבד. תיעוד המגבלות הנוכחיות בנוגע לפיצויים ותכנית לאיסוף נתוני אימון עתידיים. הגדרת קריטריונים להרחבה לפיצויים בגרסאות עתידיות. עדכון מטריקות הצלחה בהתאם למגבלות הגרסה הראשונה.",
|
||||
"testStrategy": "וולידציה שהמערכת מטפלת נכון רק בתיקי רישוי והשבחה. בדיקת התנהגות נכונה כשמתקבל תיק פיצויים.",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:01:45.879Z"
|
||||
},
|
||||
{
|
||||
"id": "61",
|
||||
"title": "בחינה מחדש של יעד 98% שיעור שינוי",
|
||||
"description": "הערכה מחדש של ריאליות יעד 98% בהתבסס על מחקר Endsley על התנהגות מומחים",
|
||||
"details": "ניתוח מחקרי על התנהגות מומחים ונטייתם לבצע שינויים. הגדרת יעד ריאלי יותר המתחשב בגורמים פסיכולוגיים. הצעת מדדי הצלחה חלופיים כמו שיעור שינויים משמעותיים או שביעות רצון המומחים. כולל הגדרת baseline מתוך נתונים היסטוריים אם קיימים.",
|
||||
"testStrategy": "ניתוח סטטיסטי של נתוני שינויים מהמערכת הנוכחית (אם קיימת). השוואה ליעדים דומים במערכות אחרות.",
|
||||
"priority": "medium",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:02:13.446Z"
|
||||
},
|
||||
{
|
||||
"id": "62",
|
||||
"title": "הגדרת מנגנון לולאת למידה",
|
||||
"description": "פיתוח מנגנון עדכון המודל על בסיס פידבק מדפנה ומשתמשים",
|
||||
"details": "הגדרת אסטרטגיית עדכון המודל: fine-tuning מול prompt engineering מול עדכון RAG. יישום מנגנון איסוף פידבק מובנה, עיבוד הנתונים לפורמט מתאים לאימון, ותהליך עדכון אוטומטי או חצי-אוטומטי. כולל מנגנון A/B testing לבדיקת שיפורים.",
|
||||
"testStrategy": "בדיקת יעילות מנגנון העדכון על דוגמאות ידועות. מדידת שיפור ביצועים לאחר עדכונים.",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"53",
|
||||
"58"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:02:32.651Z"
|
||||
},
|
||||
{
|
||||
"id": "63",
|
||||
"title": "הוספת הגנה מפני prompt injection",
|
||||
"description": "יישום מנגנון הגנה מפני prompt injection ממסמכי מקור חיצוניים",
|
||||
"details": "פיתוח מנגנון סינון וסניטיזציה של מסמכי קלט לזיהוי ניסיונות prompt injection. יישום validation של תוכן המסמכים, הפרדה בין הוראות המערכת לתוכן המסמכים, ומנגנון flagging של מסמכים חשודים. כולל רשימה שחורה של דפוסים מסוכנים.",
|
||||
"testStrategy": "בדיקות חדירה עם מסמכים המכילים ניסיונות prompt injection ידועים. וולידציה שהמערכת מזהה ומנטרלת איומים.",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"54"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:02:49.768Z"
|
||||
},
|
||||
{
|
||||
"id": "64",
|
||||
"title": "הוספת מנגנון back-flows בתהליך",
|
||||
"description": "יישום יכולת חזרה אחורה בתהליך לעריכת בלוקים קודמים או שינוי כיוון",
|
||||
"details": "פיתוח ממשק לחזרה לשלבים קודמים בתהליך. מנגנון לעריכת בלוקים שכבר הושלמו, עדכון אוטומטי של בלוקים תלויים, ומעקב אחר שינויים. כולל אזהרות למשתמש על השפעת שינויים על בלוקים אחרים ואפשרות לביטול פעולות.",
|
||||
"testStrategy": "בדיקת זרימת עבודה עם חזרות אחורה. וולידציה של עקביות הנתונים לאחר שינויים בבלוקים קודמים.",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"58"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:01:07.801Z"
|
||||
},
|
||||
{
|
||||
"id": "65",
|
||||
"title": "הוספת שלב QA/ולידציה לפני שליחה לדפנה",
|
||||
"description": "יישום checklist אוטומטי ומנגנון QA לפני הפלט הסופי",
|
||||
"details": "פיתוח checklist אוטומטי שבודק שלמות כל הבלוקים, תקינות הפורמט, נוכחות כל הרכיבים הנדרשים, ועקביות פנימית. מנגנון וולידציה של ציטוטים והפניות, בדיקת איכות השפה, ואזהרות על בעיות פוטנציאליות. כולל דוח QA מפורט למשתמש.",
|
||||
"testStrategy": "בדיקת יעילות ה-checklist על פסיקות עם בעיות ידועות. וולידציה שהמערכת תופסת שגיאות נפוצות.",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"54",
|
||||
"57"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:03:09.658Z"
|
||||
},
|
||||
{
|
||||
"id": "66",
|
||||
"title": "יישום ניהול גרסאות של בלוקים",
|
||||
"description": "פיתוח מערכת ניהול גרסאות לכל בלוק בנפרד",
|
||||
"details": "יישום version control לכל בלוק בנפרד, שמירת היסטוריית שינויים, יכולת השוואה בין גרסאות, ואפשרות לחזרה לגרסה קודמת של בלוק ספציפי. כולל ממשק גרפי להצגת ההבדלים בין גרסאות ומטא-דאטה על כל שינוי (זמן, משתמש, סיבה).",
|
||||
"testStrategy": "בדיקת שמירה ושחזור של גרסאות שונות. וולידציה של דיוק ההשוואות בין גרסאות.",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"58"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.961Z"
|
||||
},
|
||||
{
|
||||
"id": "67",
|
||||
"title": "טיפול באיחוד תיקים",
|
||||
"description": "פיתוח מנגנון לטיפול באיחוד תיקים כמו במקרה אריאלי 1078+1083",
|
||||
"details": "יישום לוגיקה לזיהוי תיקים הקשורים זה לזה ומנגנון איחוד אוטומטי או חצי-אוטומטי. טיפול בחפיפות מידע, פתרון קונפליקטים, ושמירת קישוריות בין התיקים המאוחדים. כולל ממשק למשתמש לאישור ועריכת האיחוד המוצע.",
|
||||
"testStrategy": "בדיקה על מקרי איחוד ידועים. וולידציה של שלמות המידע לאחר איחוד ותקינות הקישורים.",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"57",
|
||||
"66"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.964Z"
|
||||
},
|
||||
{
|
||||
"id": "68",
|
||||
"title": "תיקון LOA של סיעור מוחות",
|
||||
"description": "תיקון רמת האוטומציה של סיעור מוחות מרמה ג' לרמה ב'",
|
||||
"details": "עדכון הגדרת רמת האוטומציה (LOA) של תהליך סיעור המוחות מרמה ג' (אוטומציה מלאה) לרמה ב' (אוטומציה עם פיקוח אנושי). עדכון כל המסמכים והממשקים הרלוונטיים. הבטחת התאמה לרמת הביקורת הנדרשת.",
|
||||
"testStrategy": "סקירת כל המסמכים לוידוא עדכון עקבי של רמת האוטומציה. בדיקת התאמה לדרישות הביקורת.",
|
||||
"priority": "low",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.967Z"
|
||||
},
|
||||
{
|
||||
"id": "69",
|
||||
"title": "הגדרת סיעור מוחות כאופציונלי",
|
||||
"description": "שינוי הגדרת סיעור המוחות לאופציונלי גם במקרים שיש נימוק קיים",
|
||||
"details": "עדכון הלוגיקה כך שסיעור מוחות יהיה אופציונלי בכל המקרים, כולל כאשר קיים נימוק בסיסי. הוספת אפשרות למשתמש לבחור האם להפעיל סיעור מוחות או לדלג עליו. עדכון ממשק המשתמש והדרישות בהתאם.",
|
||||
"testStrategy": "בדיקת התנהגות המערכת במקרים שונים של נוכחות או היעדר נימוק. וולידציה של חופש הבחירה של המשתמש.",
|
||||
"priority": "low",
|
||||
"dependencies": [
|
||||
"68"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.969Z"
|
||||
},
|
||||
{
|
||||
"id": "70",
|
||||
"title": "הוספת ניטרליות מבנית",
|
||||
"description": "הרחבת דרישות הניטרליות מלקסיקלית למבנית",
|
||||
"details": "הגדרת כללים לניטרליות מבנית בנוסף ללקסיקלית: סדר הצגת הטיעונים, אורך היחסי של סעיפים, מיקום המידע, ומבנה הפסיקה. פיתוח מנגנון בדיקה אוטומטית לזיהוי הטיה מבנית ואזהרות למשתמש. כולל הנחיות לכתיבה מאוזנת.",
|
||||
"testStrategy": "ניתוח פסיקות לזיהוי דפוסי הטיה מבנית. בדיקת יעילות המנגנון בזיהוי וטיפול בהטיות.",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"57"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.973Z"
|
||||
},
|
||||
{
|
||||
"id": "71",
|
||||
"title": "מיפוי פרסורמן 4 stages",
|
||||
"description": "הרחבת המיפוי מ-LOA בלבד לכלל 4 השלבים של מודל פרסורמן",
|
||||
"details": "מיפוי מלא של התהליך לפי 4 השלבים של פרסורמן: Information acquisition, Information analysis, Decision selection, Action implementation. הגדרת רמת האוטומציה לכל שלב בנפרד ולא רק LOA כללי. עדכון התיעוד והדרישות בהתאם.",
|
||||
"testStrategy": "וולידציה של המיפוי מול מודל פרסורמן המקורי. בדיקת עקביות ההגדרות בין השלבים השונים.",
|
||||
"priority": "low",
|
||||
"dependencies": [
|
||||
"68"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.976Z"
|
||||
},
|
||||
{
|
||||
"id": "72",
|
||||
"title": "הגדרת דרישות ביצועים per-block וסינכרוני/אסינכרוני",
|
||||
"description": "הגדרת דרישות ביצועים מפורטות לכל בלוק ובחירה בין עיבוד סינכרוני לאסינכרוני",
|
||||
"details": "הגדרת SLA ספציפי לכל בלוק: זמני תגובה מקסימליים, throughput נדרש, ושיעור זמינות. החלטה על ארכיטקטורת עיבוד: סינכרונית לבלוקים קריטיים, אסינכרונית לבלוקים כבדים. יישום מנגנון ניטור ביצועים ואזהרות על חריגה מהסטנדרטים.",
|
||||
"testStrategy": "בדיקות עומס לכל בלוק בנפרד. מדידת זמני תגובה ותפוקה בתנאים שונים. וולידציה של עמידה ב-SLA.",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"57"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-02T21:04:33.980Z"
|
||||
},
|
||||
{
|
||||
"id": "73",
|
||||
"title": "הרחבת DB schema לתהליך מלא",
|
||||
"description": "הוספת שדות וטבלאות חסרים לתמיכה בתהליך המלא של כתיבת החלטות משפטיות",
|
||||
"details": "בקובץ db.py:\n1. הוספת שדות לטבלת decisions:\n - direction_doc JSONB - לשמירת מסמך הכיוון\n - outcome_reasoning TEXT - לנימוק התוצאה\n2. הרחבת enum של status בטבלת cases ל-13 ערכים:\n ['new', 'uploading', 'processing', 'documents_ready', 'outcome_set', 'brainstorming', 'direction_approved', 'drafting', 'qa_review', 'drafted', 'exported', 'reviewed', 'final']\n3. יצירת טבלת qa_results חדשה:\n - id SERIAL PRIMARY KEY\n - case_number VARCHAR REFERENCES cases\n - validation_type VARCHAR\n - passed BOOLEAN\n - errors JSONB\n - created_at TIMESTAMP\n4. יישום כ-migration עם Alembic",
|
||||
"testStrategy": "1. בדיקת migration up/down\n2. וידוא שכל השדות החדשים נוצרו\n3. בדיקת constraints ו-foreign keys\n4. בדיקת ערכי enum החדשים\n5. בדיקת insert/update על השדות החדשים",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T08:54:55.256Z"
|
||||
},
|
||||
{
|
||||
"id": "74",
|
||||
"title": "הוספת 5 API endpoints חדשים ב-MCP server",
|
||||
"description": "יצירת endpoints חדשים לתמיכה בתהליך כתיבת ההחלטות",
|
||||
"details": "בקובץ server.py או בקבצי API:\n1. POST /api/cases/{case_number}/outcome\n - קבלת: {outcome: string, reasoning: string}\n - שמירה ב-DB\n - עדכון סטטוס ל-outcome_set\n2. GET /api/cases/{case_number}/claims\n - החזרת טענות מחולצות מה-JSONB\n3. POST /api/cases/{case_number}/direction\n - קבלת מסמך כיוון כ-JSON\n - שמירה בשדה direction_doc\n - עדכון סטטוס ל-direction_approved\n4. POST /api/cases/{case_number}/qa\n - הרצת בדיקות QA\n - שמירה בטבלת qa_results\n - החזרת תוצאות\n5. POST /api/cases/{case_number}/learn\n - הפעלת לולאת למידה\n - עדכון מודלים/פרמטרים",
|
||||
"testStrategy": "1. בדיקת כל endpoint עם Postman/pytest\n2. בדיקת validations על הקלט\n3. בדיקת error handling\n4. בדיקת עדכוני סטטוס\n5. בדיקת permissions/auth",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"73"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T08:55:56.839Z"
|
||||
},
|
||||
{
|
||||
"id": "75",
|
||||
"title": "הוספת 8 tools חדשים לפלאגין Paperclip",
|
||||
"description": "הרחבת הפלאגין עם כלים חדשים לאינטראקציה עם המערכת המשפטית",
|
||||
"details": "1. בקובץ src/worker.ts - הוספת 8 tools:\n - legal_document_upload: העלאת מסמך\n - legal_document_list: רשימת מסמכים\n - legal_document_text: קריאת טקסט ממסמך\n - legal_search_case: חיפוש תיק\n - legal_find_similar: מציאת תקדימים\n - legal_set_outcome: הגדרת תוצאה\n - legal_get_claims: קבלת טענות\n - legal_style_guide: קבלת הנחיות סגנון\n\n2. בקובץ src/legal-api.ts - יישום 8 methods:\n ```typescript\n async uploadDocument(caseNumber: string, file: File) {...}\n async listDocuments(caseNumber: string) {...}\n async getDocumentText(docId: string) {...}\n async searchCase(query: string) {...}\n async findSimilar(caseNumber: string) {...}\n async setOutcome(caseNumber: string, outcome: string, reasoning: string) {...}\n async getClaims(caseNumber: string) {...}\n async getStyleGuide() {...}\n ```\n\n3. בקובץ plugin.json - עדכון manifest",
|
||||
"testStrategy": "1. בדיקת כל tool בנפרד\n2. בדיקת error handling\n3. בדיקת response format\n4. בדיקת אינטגרציה עם Claude\n5. בדיקת performance",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"74"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T08:59:27.838Z"
|
||||
},
|
||||
{
|
||||
"id": "76",
|
||||
"title": "שיפור status sync ב-Paperclip",
|
||||
"description": "מיפוי מלא של 13 סטטוסים והוספת comments מפורטים",
|
||||
"details": "1. עדכון מיפוי סטטוסים:\n ```javascript\n const statusMapping = {\n 'new': 'תיק חדש',\n 'uploading': 'העלאת מסמכים',\n 'processing': 'עיבוד מסמכים',\n 'documents_ready': 'מסמכים מוכנים',\n 'outcome_set': 'תוצאה הוגדרה',\n 'brainstorming': 'גיבוש כיוון',\n 'direction_approved': 'כיוון אושר',\n 'drafting': 'כתיבת החלטה',\n 'qa_review': 'בדיקת איכות',\n 'drafted': 'טיוטה מוכנה',\n 'exported': 'יוצאה ל-DOCX',\n 'reviewed': 'נבדקה ע\"י עו\"ד',\n 'final': 'סופית'\n }\n ```\n\n2. הוספת comments אוטומטיים ב-Paperclip:\n - בכל מעבר סטטוס\n - עם timestamp\n - עם פירוט הפעולה\n\n3. עדכון job sync-case-status",
|
||||
"testStrategy": "1. בדיקת כל מעבר סטטוס\n2. וידוא comments נוצרים\n3. בדיקת sync דו-כיווני\n4. בדיקת edge cases\n5. בדיקת performance עם הרבה עדכונים",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"73"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T09:00:19.243Z"
|
||||
},
|
||||
{
|
||||
"id": "77",
|
||||
"title": "כתיבת SOUL.md לסוכנים",
|
||||
"description": "יצירת קבצי הנחיות לסוכני AI בעברית",
|
||||
"details": "1. CEO Agent SOUL.md:\n ```markdown\n # CEO Agent - סוכן מנהל\n \n ## תפקיד\n ניהול תהליך כתיבת החלטה משפטית מקצה לקצה\n \n ## הנחיות\n - עבוד בעברית תמיד\n - נהל את התהליך לפי 13 הסטטוסים\n - התרע לחיים במקרים: תקלה טכנית, החלטה מורכבת, חריגה מזמנים\n - וודא שכל שלב הושלם לפני מעבר לבא\n \n ## מיפוי סטטוסים\n [רשימת 13 סטטוסים עם הסבר לכל אחד]\n ```\n\n2. Case Analyst Agent SOUL.md:\n ```markdown\n # Case Analyst - סוכן מנתח\n \n ## תפקיד\n ניתוח מסמכים משפטיים וחילוץ מידע\n \n ## הנחיות\n - נתח מסמכים בעברית\n - חלץ טענות מרכזיות\n - זהה תקדימים רלוונטיים\n - סכם עובדות מהותיות\n ```",
|
||||
"testStrategy": "1. בדיקת קריאות והבנה\n2. בדיקת שהסוכנים פועלים לפי ההנחיות\n3. בדיקת תגובות בעברית\n4. בדיקת זיהוי מצבי התראה\n5. בדיקת מעברי סטטוס",
|
||||
"priority": "medium",
|
||||
"dependencies": [],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T08:57:14.984Z"
|
||||
},
|
||||
{
|
||||
"id": "78",
|
||||
"title": "יישום skill /brainstorm",
|
||||
"description": "יצירת skill לגיבוש כיוון ההחלטה בשיתוף עם המשתמש",
|
||||
"details": "בקובץ skills/brainstorm.ts:\n```typescript\nexport async function brainstorm(caseNumber: string) {\n // שלב 1: הצגת טענות מרכזיות\n const claims = await api.getClaims(caseNumber);\n displayClaims(claims);\n \n // שלב 2: הצעת 2-3 כיוונים\n const directions = generateDirections(claims);\n displayDirections(directions);\n \n // שלב 3: דיון אינטראקטיבי\n let approved = false;\n while (!approved) {\n const feedback = await getUserFeedback();\n if (feedback.type === 'approve') {\n approved = true;\n } else {\n directions = refineDirections(directions, feedback);\n }\n }\n \n // שלב 4: יצירת מסמך כיוון\n const directionDoc = {\n mainDirection: directions.selected,\n keyPoints: directions.keyPoints,\n precedents: directions.precedents,\n approvedBy: 'user',\n timestamp: new Date()\n };\n \n // שלב 5: שמירה ועדכון סטטוס\n await api.saveDirection(caseNumber, directionDoc);\n}\n```",
|
||||
"testStrategy": "1. בדיקת תצוגת טענות\n2. בדיקת יצירת כיוונים\n3. בדיקת אינטראקציה\n4. בדיקת שמירת מסמך כיוון\n5. בדיקת חסימה - אין התקדמות בלי אישור",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"74"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:16:24.667Z"
|
||||
},
|
||||
{
|
||||
"id": "79",
|
||||
"title": "שיפור skill /draft-decision לכתיבה בלוק-אחרי-בלוק",
|
||||
"description": "שדרוג מ-stub לכתיבה מלאה עם 12 בלוקים",
|
||||
"details": "בקובץ skills/draft-decision.ts:\n```typescript\nconst BLOCKS = [\n {id: 'ה', name: 'כותרת', temperature: 0.3},\n {id: 'ו', name: 'פתיח', temperature: 0.5},\n {id: 'ז', name: 'רקע', temperature: 0.4},\n {id: 'ח', name: 'טענות הצדדים', temperature: 0.3},\n {id: 'ט', name: 'תמצית', temperature: 0.6},\n {id: 'י', name: 'דיון והכרעה', temperature: 0.7, model: 'opus'},\n {id: 'יא', name: 'סוף דבר', temperature: 0.5}\n];\n\nexport async function draftDecision(caseNumber: string) {\n const direction = await api.getDirection(caseNumber);\n const lastBlock = await getLastCompletedBlock(caseNumber);\n \n for (let i = getBlockIndex(lastBlock) + 1; i < BLOCKS.length; i++) {\n const block = BLOCKS[i];\n \n // כתיבת בלוק\n const content = await writeBlock(block, {\n direction,\n previousBlocks: await getPreviousBlocks(caseNumber, i),\n temperature: block.temperature,\n model: block.model || 'default'\n });\n \n // שמירה מיידית\n await saveBlock(caseNumber, block.id, content);\n \n // בלוק י - CREAC + thinking\n if (block.id === 'י') {\n await applyCREAC(content);\n await addThinkingTags(content);\n }\n }\n}\n\n// Recovery function\nexport async function recoverDraft(caseNumber: string) {\n const lastBlock = await getLastCompletedBlock(caseNumber);\n return draftDecision(caseNumber); // ממשיך מאיפה שנפל\n}\n```",
|
||||
"testStrategy": "1. בדיקת כתיבה רציפה של כל הבלוקים\n2. בדיקת recovery אחרי נפילה\n3. בדיקת CREAC בבלוק י\n4. בדיקת שמירה אחרי כל בלוק\n5. בדיקת פרמטרים שונים לכל בלוק",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"78",
|
||||
"73"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:16:24.670Z"
|
||||
},
|
||||
{
|
||||
"id": "80",
|
||||
"title": "יישום skill /qa-validate",
|
||||
"description": "בדיקות איכות אוטומטיות על ההחלטה",
|
||||
"details": "בקובץ skills/qa-validate.ts:\n```typescript\nexport async function qaValidate(caseNumber: string) {\n const decision = await api.getDecision(caseNumber);\n const documents = await api.getDocuments(caseNumber);\n const claims = await api.getClaims(caseNumber);\n \n const checks = [\n {\n name: 'grounding_check',\n fn: () => validateGrounding(decision, documents),\n critical: true\n },\n {\n name: 'claims_coverage',\n fn: () => validateClaimsCoverage(decision, claims),\n critical: true\n },\n {\n name: 'neutral_background',\n fn: () => validateNeutrality(decision.background),\n critical: false\n },\n {\n name: 'weights_range',\n fn: () => validateWeightsInRange(decision),\n critical: true\n },\n {\n name: 'sequential_numbering',\n fn: () => validateNumbering(decision),\n critical: false\n },\n {\n name: 'definitions',\n fn: () => validateDefinitions(decision),\n critical: false\n }\n ];\n \n const results = [];\n let hasErrors = false;\n \n for (const check of checks) {\n const result = await check.fn();\n results.push({...result, name: check.name});\n if (!result.passed && check.critical) {\n hasErrors = true;\n }\n }\n \n // שמירת תוצאות\n await api.saveQAResults(caseNumber, results);\n \n // חסימת ייצוא אם יש שגיאות קריטיות\n if (hasErrors) {\n await api.blockExport(caseNumber);\n throw new Error('QA failed - export blocked');\n }\n \n return results;\n}\n```",
|
||||
"testStrategy": "1. בדיקת כל validation בנפרד\n2. בדיקת חסימת ייצוא\n3. בדיקת דוח שגיאות מפורט\n4. בדיקת false positives\n5. בדיקת performance",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"79"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:16:24.673Z"
|
||||
},
|
||||
{
|
||||
"id": "81",
|
||||
"title": "אינטגרציה E2E וחיבור Paperclip events",
|
||||
"description": "חיבור מלא בין Paperclip ל-Claude Code עם trigger אוטומטי",
|
||||
"details": "1. חיבור Paperclip events:\n```javascript\n// בקובץ paperclip-integration.js\npaperclip.on('issue.comment.created', async (event) => {\n if (event.comment.includes('/draft')) {\n await claudeCode.trigger('draft-decision', {\n caseNumber: event.issue.number\n });\n }\n});\n```\n\n2. E2E test על תיק הכט:\n```javascript\ntest('full flow - Hecht case', async () => {\n // העלאת חומרים\n await uploadDocuments('hecht', ['doc1.pdf', 'doc2.pdf']);\n \n // הזנת תוצאה\n await setOutcome('hecht', 'rejected', 'אין עילה');\n \n // כתיבה\n await triggerDraft('hecht');\n await waitForStatus('drafted');\n \n // QA\n const qaResults = await runQA('hecht');\n expect(qaResults.passed).toBe(true);\n \n // ייצוא\n const docx = await exportToDocx('hecht');\n \n // השוואה\n const similarity = await compareToFinal(docx, 'hecht-final.docx');\n expect(similarity).toBeGreaterThan(0.9);\n});\n```",
|
||||
"testStrategy": "1. בדיקת trigger מ-Paperclip\n2. בדיקת flow מלא על תיק אמיתי\n3. בדיקת error handling\n4. בדיקת recovery\n5. השוואה להחלטה סופית",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"75",
|
||||
"76",
|
||||
"77",
|
||||
"78",
|
||||
"79",
|
||||
"80"
|
||||
],
|
||||
"status": "deferred",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:19:26.776Z"
|
||||
},
|
||||
{
|
||||
"id": "82",
|
||||
"title": "מבחן הסמכה",
|
||||
"description": "בדיקת המערכת על תיק עם החלטה קיימת והשוואת איכות",
|
||||
"details": "שלב ב - בדיקה על תיק עם החלטה:\n```javascript\nexport async function certificationTest() {\n // בחירת תיק עם החלטה סופית\n const testCase = await selectTestCase();\n \n // הסתרת ההחלטה המקורית\n await hideOriginalDecision(testCase.number);\n \n // הרצת המערכת\n await runFullFlow(testCase.number);\n \n // השוואה\n const draft = await getDecision(testCase.number);\n const original = testCase.originalDecision;\n \n const comparison = {\n structure: compareStructure(draft, original),\n content: compareContent(draft, original),\n reasoning: compareReasoning(draft, original),\n outcome: compareOutcome(draft, original)\n };\n \n // חישוב ציון כולל\n const score = calculateScore(comparison);\n \n // בדיקת סף - 90%\n if (score < 0.9) {\n throw new Error(`Score ${score} is below threshold`);\n }\n \n return {score, comparison};\n}\n\n// שלב ג - תיק חי\nexport async function liveTest() {\n const liveCase = await getLiveCase();\n await runFullFlow(liveCase.number);\n \n // שליחה לדפנה לבדיקה\n await sendForReview('dafna@law.firm', liveCase.number);\n}\n```",
|
||||
"testStrategy": "1. בדיקת בחירת תיק מתאים\n2. בדיקת הסתרת החלטה מקורית\n3. בדיקת אלגוריתם השוואה\n4. בדיקת חישוב ציון\n5. בדיקת תהליך review עם דפנה",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"81"
|
||||
],
|
||||
"status": "deferred",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-03T10:19:26.779Z"
|
||||
},
|
||||
{
|
||||
"id": "83",
|
||||
"title": "Phase 1 — Project setup (legal-ai UI rewrite)",
|
||||
"description": "הקמת scaffold של Next.js עם TypeScript + Tailwind v4 + App Router ב-web-ui/. התקנת כל התלויות: @tanstack/react-query, @tanstack/react-table, react-hook-form, @hookform/resolvers, zod, lucide-react, react-dropzone, openapi-typescript. העברת design-system.css tokens (navy/gold/parchment, Heebo) ל-Tailwind theme דרך @theme ו-CSS variables. הגדרת RTL עברית עם Heebo via next/font/google. בניית AppShell עם navy header + gold rule + nav.",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"priority": "high",
|
||||
"details": "**השלמות (2026-04-11):**\n\n✅ **Scaffold:** Next.js 16.2.3 (חדש יותר מ-v15 שתוכנן), React 19.2.4, Tailwind v4, Turbopack.\n\n✅ **תלויות מותקנות** (`web-ui/package.json:11-21`):\n- @tanstack/react-query ^5.97.0\n- @tanstack/react-table ^8.21.3\n- react-hook-form ^7.72.1 + @hookform/resolvers ^5.2.2\n- zod ^4.3.6\n- lucide-react ^1.8.0\n- react-dropzone ^15.0.0\n- openapi-typescript ^7.13.0 (devDep)\n\n✅ **Design tokens** (`web-ui/src/app/globals.css:10-107`): Tailwind v4 @theme עם כל הצבעים (navy, cream, parchment, gold, ink, status colors), radii, shadows, fonts, dark mode preserved.\n\n✅ **RTL Hebrew** (`web-ui/src/app/layout.tsx:5-10, 23`): Heebo עם hebrew+latin subsets, `lang=\"he\" dir=\"rtl\"` on html.\n\n✅ **AppShell** (`web-ui/src/components/app-shell.tsx:29-70`): Navy header עם gold border-b-3, RTL nav, parchment body.\n\n✅ **Home page placeholder** (`web-ui/src/app/page.tsx`).\n\n✅ **Build:** `npm run build` עובר ב-3.8s, 0 errors, static.\n\n**נותר:** אישור ויזואלי של המשתמש עם `npm run dev`.\n\n**תוכנית מלאה:** `~/.claude/plans/joyful-marinating-sutton.md`",
|
||||
"testStrategy": "1. `cd web-ui && npm run dev` — פתיחה ב-http://localhost:3000\n2. וידוא ויזואלי: Header navy עם gold rule, RTL rendering, פונט Heebo טעון\n3. השוואה ל-legal-ai.nautilus.marcusgroup.org — אותו מראה header\n4. בדיקת dark mode (toggle class על html)\n5. אישור סופי מהמשתמש",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "יצירת Next.js 16 scaffold עם TypeScript + Tailwind v4 + App Router",
|
||||
"description": "הרצת create-next-app ב-web-ui/ עם App Router, TypeScript, Tailwind v4, ESLint",
|
||||
"dependencies": [],
|
||||
"details": "Next.js 16.2.3 (חדש יותר מ-v15), React 19.2.4, Tailwind v4, Turbopack. קבצים: web-ui/package.json, tsconfig.json, next.config.ts",
|
||||
"status": "done",
|
||||
"testStrategy": "npm run build succeeds",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "התקנת כל התלויות הנדרשות",
|
||||
"description": "npm install של @tanstack/react-query, @tanstack/react-table, react-hook-form, @hookform/resolvers, zod, lucide-react, react-dropzone, openapi-typescript",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"details": "ראה web-ui/package.json:11-21 לגרסאות המותקנות. כולל openapi-typescript כ-devDep לשלב 2.",
|
||||
"status": "done",
|
||||
"testStrategy": "npm ls shows all packages",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "העברת design tokens ל-Tailwind v4 @theme",
|
||||
"description": "פורט מלא של design-system.css ל-globals.css עם Tailwind v4 @theme syntax",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"details": "web-ui/src/app/globals.css:10-107 כולל כל הצבעים (navy, cream, parchment, gold, ink), status colors, radii, shadows, fonts, dark mode. CSS variables עובדים עם Tailwind classes.",
|
||||
"status": "done",
|
||||
"testStrategy": "Tailwind classes like bg-navy, text-gold work correctly",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "הגדרת RTL Hebrew עם Heebo font",
|
||||
"description": "next/font/google Heebo עם hebrew+latin, lang=he dir=rtl על html",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"details": "web-ui/src/app/layout.tsx:5-10 — Heebo עם weights 300-900, display swap. שורה 23: html lang=he dir=rtl.",
|
||||
"status": "done",
|
||||
"testStrategy": "Page renders RTL, Heebo font loaded",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "בניית AppShell component עם navy header + gold rule",
|
||||
"description": "רכיב shell עם header navy, gold border, RTL nav, parchment body",
|
||||
"dependencies": [
|
||||
3,
|
||||
4
|
||||
],
|
||||
"details": "web-ui/src/components/app-shell.tsx:29-70 — Header עם bg-navy, border-b-3 border-gold, nav links (בית, העלאת מסמכים, אימון סגנון, מיומנויות, אבחון), main content area עם max-w-1400px.",
|
||||
"status": "done",
|
||||
"testStrategy": "Visual match to current header",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "יצירת דף בית placeholder",
|
||||
"description": "דף page.tsx עם AppShell ו-placeholder content",
|
||||
"dependencies": [
|
||||
5
|
||||
],
|
||||
"details": "web-ui/src/app/page.tsx:1-27 — דף בית עם כותרת 'עוזר משפטי', תיאור המערכת, gold gradient divider, כרטיס סטטוס.",
|
||||
"status": "done",
|
||||
"testStrategy": "npm run build succeeds (done: 3.8s, 0 errors)",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "אישור ויזואלי מהמשתמש — npm run dev",
|
||||
"description": "הרצת dev server ואישור סופי שה-UI תואם לציפיות — header, RTL, fonts, colors",
|
||||
"dependencies": [
|
||||
6
|
||||
],
|
||||
"details": "המשתמש צריך להריץ 'cd web-ui && npm run dev' ולאשר שהכל נראה כמו legal-ai.nautilus.marcusgroup.org. בדיקת dark mode אופציונלית.",
|
||||
"status": "pending",
|
||||
"testStrategy": "User confirms visual parity with current site, RTL works, Heebo font loads",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-04-11T13:50:47.941Z"
|
||||
},
|
||||
{
|
||||
"id": "84",
|
||||
"title": "Phase 2 — API client + generated TypeScript types",
|
||||
"description": "Add npm run api:types script that runs openapi-typescript against FastAPI's /openapi.json -> src/lib/api/types.ts. Build lib/api/client.ts (typed fetch wrapper + TanStack Query client with default retry/staleTime). Create one lib/api/<domain>.ts per endpoint category (cases, upload, compose, training, system), each exporting typed useQuery/useMutation hooks. Build lib/sse.ts as EventSource -> Query cache adapter. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 2 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "useCases() hook returns typed array from live FastAPI. TypeScript errors if backend endpoint changes without frontend update.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"83"
|
||||
],
|
||||
"priority": "high",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T15:51:34.020Z"
|
||||
},
|
||||
{
|
||||
"id": "85",
|
||||
"title": "Phase 3 — Core read views (home, case detail, compose)",
|
||||
"description": "Port the 3 highest-value screens. Use the frontend-design Claude Code skill to generate layout + composition, passing design tokens (navy/gold/parchment, Heebo), editorial voice, and typed API hooks. Use shadcn Card/Badge/Tabs/Sheet/ScrollArea as primitives. Port the custom donut chart into <DonutChart> component. TanStack Query staleTime:5000 for case detail replaces manual 5s polling. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 3 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Users can browse case list, open a case detail, and view the compose screen with live data from FastAPI. All 3 screens visually match the existing legal-ai identity.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"84"
|
||||
],
|
||||
"priority": "high",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T16:09:18.006Z"
|
||||
},
|
||||
{
|
||||
"id": "86",
|
||||
"title": "Phase 4 — Forms and wizards (new case, upload, inline edits)",
|
||||
"description": "Port new case wizard, bulk upload, inline forms on case detail. Use react-hook-form + zod with schemas in lib/schemas/<entity>.ts. Build shared <WizardShell> from shadcn Card + Progress + Tabs. Build <DropZone> (react-dropzone + shadcn). Integrate SSE for upload progress via lib/sse.ts. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 4 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Users can create a new case via the multi-step wizard (case appears in Gitea + Paperclip), upload documents with live SSE progress, and edit case fields inline.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"85"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T16:25:55.569Z"
|
||||
},
|
||||
{
|
||||
"id": "87",
|
||||
"title": "Phase 5 — Secondary screens (compare, training, style report, skills, diagnostics)",
|
||||
"description": "Port the remaining 5 views. Use TanStack Table for training corpus and diagnostics lists. Port any charts/visualizations from current index.html. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 5 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Feature parity with old legal-ai/web/static/index.html across all 10 views.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"86"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T17:33:42.976Z"
|
||||
},
|
||||
{
|
||||
"id": "88",
|
||||
"title": "Phase 6 — Polish & testing",
|
||||
"description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"87"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T17:44:08.337Z"
|
||||
},
|
||||
{
|
||||
"id": "89",
|
||||
"title": "Phase 7 — Deployment & cutover",
|
||||
"description": "Add multi-stage Dockerfile for web-ui/ (Node 20 build -> nginx serve of out/). Add web-ui as new app in Coolify project pointing to staging subdomain legal-ai-next.nautilus.marcusgroup.org. Run full smoke test against staging. Cutover: DNS flip legal-ai.nautilus.marcusgroup.org to new app, keep old on rollback subdomain for 1 week. Follow-up PR removes legal-ai/web/static/index.html + design-system.css once stable. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 7 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "legal-ai.nautilus.marcusgroup.org serves the new Next.js UI in production. Old UI accessible on rollback subdomain for 7 days. SSE streams working through Coolify proxy.",
|
||||
"status": "pending",
|
||||
"dependencies": [
|
||||
"88"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": []
|
||||
},
|
||||
{
|
||||
"id": "90",
|
||||
"title": "Phase 4.5 — Practice area integration",
|
||||
"description": "Add practice_area + appeal_subtype to the wizard, types, schema, case header, and cases table. Gap identified after backend commit 26d09d6 (multi-tenant axis) — new Next.js UI has zero integration while vanilla UI is fully wired. Plan: ~/.claude/plans/woolly-cooking-graham.md",
|
||||
"details": "",
|
||||
"testStrategy": "",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"86"
|
||||
],
|
||||
"priority": "high",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T17:15:57.831Z"
|
||||
},
|
||||
{
|
||||
"id": "91",
|
||||
"title": "Precedent attachment in compose screen",
|
||||
"description": "Add case_precedents table + FastAPI endpoints + MCP tools + Next.js compose UI for attaching legal precedents (quote + citation + optional archived PDF) to threshold_claims/issues and to the case as a whole. Plan: ~/.claude/plans/woolly-cooking-graham.md",
|
||||
"details": "",
|
||||
"testStrategy": "",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"priority": "high",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T19:20:56.040Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-04-11T19:20:56.040Z",
|
||||
"taskCount": 60,
|
||||
"completedCount": 57,
|
||||
"tags": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
149
CLAUDE.md
149
CLAUDE.md
@@ -1,48 +1,123 @@
|
||||
# עוזר משפטי (Ezer Mishpati)
|
||||
# עוזר משפטי — Legal Decision Assistant
|
||||
|
||||
מערכת AI לסיוע בניסוח החלטות משפטיות בסגנון דפנה תמיר, יו"ר ועדת הערר מחוז ירושלים.
|
||||
## רקע הפרויקט
|
||||
|
||||
## כלי MCP זמינים
|
||||
מערכת AI לסיוע בכתיבת החלטות של **ועדת ערר לתכנון ובניה, מחוז ירושלים**, בראשות **עו"ד דפנה תמיר**.
|
||||
|
||||
### ניהול תיקים
|
||||
- `case_create` - יצירת תיק ערר חדש
|
||||
- `case_list` - רשימת תיקים (סינון אופציונלי לפי סטטוס)
|
||||
- `case_get` - פרטי תיק מלאים כולל מסמכים
|
||||
- `case_update` - עדכון פרטי תיק וסטטוס
|
||||
### מה עושה ועדת ערר?
|
||||
ועדת ערר היא גוף מעין-שיפוטי שדן בעררים על החלטות ועדות מקומיות לתכנון ובניה. הוועדה מקבלת חומרי מקור (כתבי ערר, תגובות, פרוטוקולים, תכניות), דנה בטענות הצדדים, ומוציאה **החלטה כתובה מנומקת** — מסמך משפטי פורמלי שניתן לביקורת שיפוטית בבית משפט לעניינים מנהליים.
|
||||
|
||||
### מסמכים
|
||||
- `document_upload` - העלאה ועיבוד מסמך (חילוץ טקסט → chunks → embeddings)
|
||||
- `document_upload_training` - העלאת החלטה קודמת של דפנה לקורפוס
|
||||
- `document_get_text` - קבלת טקסט מחולץ
|
||||
- `document_list` - רשימת מסמכים בתיק
|
||||
### שלושה סוגי עררים
|
||||
| סוג | מספרי תיקים | טון | מאפיין |
|
||||
|-----|-------------|-----|--------|
|
||||
| רישוי ובנייה | 1xxx | חם יחסית | הקשר תכנוני רחב, אלמנטים אנושיים |
|
||||
| היטל השבחה | 8xxx | קר ומקצועי | יבש, ללא רגשות |
|
||||
| פיצויים (ס' 197) | 9xxx | קר ומקצועי | דומה להיטל השבחה |
|
||||
|
||||
### חיפוש
|
||||
- `search_decisions` - חיפוש סמנטי בהחלטות ומסמכים
|
||||
- `search_case_documents` - חיפוש בתוך תיק ספציפי
|
||||
- `find_similar_cases` - מציאת תיקים דומים
|
||||
### מטרת המערכת
|
||||
לבנות כלי עבודה שמסייע ליו"ר הוועדה לנסח החלטות:
|
||||
1. **ניהול תיקים** — ייבוא חומרי מקור, סיווג מסמכים, מעקב סטטוס
|
||||
2. **בסיס ידע** — פסיקה, ביטויי מעבר, לקחים מהחלטות קודמות, חקיקה
|
||||
3. **חיפוש סמנטי (RAG)** — מציאת תקדימים רלוונטיים ופסקאות דומות
|
||||
4. **סיוע בכתיבה** — ייצור טיוטות לפי ארכיטקטורת 12 בלוקים בסגנון דפנה
|
||||
5. **ייצוא DOCX** — מסמך מעוצב מוכן להגשה
|
||||
|
||||
### ניסוח
|
||||
- `get_style_guide` - דפוסי הסגנון של דפנה
|
||||
- `draft_section` - הרכבת הקשר לניסוח סעיף (עובדות + תקדימים + סגנון)
|
||||
- `get_decision_template` - תבנית מבנית להחלטה
|
||||
- `analyze_style` - ניתוח סגנון על הקורפוס
|
||||
### מה היה קודם (Legacy)
|
||||
המערכת הקודמת היתה **Obsidian vault** עם Claude Code skills על שרת אחר. פותחו:
|
||||
- ניתוח סגנון של 3 החלטות (הכט — דחייה, בית הכרם — קבלה חלקית, אריאלי — השוואה)
|
||||
- ארכיטקטורת 12 בלוקים מבוססת CREAC / DITA / Akoma Ntoso / Federal Judicial Center
|
||||
- כללי כתיבה (רקע ניטרלי, ללא כפילות, טענות מקוריות בלבד)
|
||||
- לקחים מהשוואת טיוטות לגרסאות סופיות
|
||||
- סקריפט ייצוא DOCX
|
||||
|
||||
### תהליך עבודה
|
||||
- `workflow_status` - סטטוס מלא לתיק
|
||||
- `processing_status` - סטטוס כללי של המערכת
|
||||
כל החומר הועבר לתיקיית `legacy/` כקריאה בלבד. **הפרויקט הנוכחי** מעביר את הידע הזה למערכת מובנית עם PostgreSQL + pgvector + n8n.
|
||||
|
||||
## תהליך עבודה טיפוסי
|
||||
---
|
||||
|
||||
1. `/new-case` → יצירת תיק חדש
|
||||
2. `/upload-doc` → העלאת כתב ערר ותשובת ועדה
|
||||
3. חיפוש תיקים דומים
|
||||
4. `/draft-decision` → ניסוח סעיף אחר סעיף
|
||||
5. עריכה ושיפור עם Claude
|
||||
6. עדכון סטטוס → final
|
||||
## מסמכי ייחוס
|
||||
|
||||
## הנחיות ניסוח
|
||||
| מסמך | תוכן | מתי לקרוא |
|
||||
|------|-------|-----------|
|
||||
| [`docs/architecture.md`](docs/architecture.md) | ארכיטקטורת המערכת, תרשים רכיבים, זרימת נתונים, 4 שכבות DB | לפני עבודה על תשתית |
|
||||
| [`docs/block-schema.md`](docs/block-schema.md) | הגדרת 12 בלוקים — content model, constraints, processing params | **לפני כל כתיבת החלטה** |
|
||||
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
|
||||
| [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
|
||||
| [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** |
|
||||
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
|
||||
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
|
||||
|
||||
- כל ההחלטות בעברית
|
||||
- שמור על סגנון דפנה (השתמש ב-`get_style_guide` לפני ניסוח)
|
||||
- הפנה לתקדימים מהקורפוס
|
||||
- המבנה: רקע → טענות עוררים → טענות משיבים → דיון → מסקנה → החלטה
|
||||
---
|
||||
|
||||
## שרת Nautilus (158.178.131.193)
|
||||
|
||||
| שירות | תפקיד | כתובת |
|
||||
|-------|--------|-------|
|
||||
| Coolify | ניהול containers | `http://158.178.131.193:8000` |
|
||||
| PostgreSQL + pgvector | בסיס נתונים ראשי | `legal-ai-postgres` |
|
||||
| Redis | תור משימות | `legal-ai-redis` |
|
||||
| n8n | אוטומציית workflows | להגדרה |
|
||||
| Gitea | מאגר קוד | `gitea.nautilus.marcusgroup.org/ezer-mishpati` |
|
||||
| ezer-mishpati-web | ממשק העלאת מסמכים | `legal-ai.nautilus.marcusgroup.org` |
|
||||
| Infisical | ניהול סודות | `secret.dev.marcus-law.co.il` |
|
||||
|
||||
---
|
||||
|
||||
## מבנה תיקיות
|
||||
|
||||
```
|
||||
/home/chaim/legal-ai/
|
||||
├── CLAUDE.md ← הקובץ הזה
|
||||
├── Dockerfile ← Docker build
|
||||
├── docs/ ← תיעוד + לקחים
|
||||
│ ├── architecture.md ארכיטקטורה
|
||||
│ ├── block-schema.md 12 בלוקים (המסמך החשוב ביותר)
|
||||
│ ├── migration-plan.md תוכנית מעבר vault → DB
|
||||
│ ├── legal-decision-lessons.md לקחים מ-3 החלטות
|
||||
│ └── memory.md הקשר כללי — skills, פרויקטים
|
||||
├── skills/ ← כלי עבודה ומדריכים
|
||||
│ ├── decision/ מדריך סגנון + references + 12 בלוקים
|
||||
│ ├── assistant/ קטלוג מסמכים
|
||||
│ └── docx/ עיצוב DOCX
|
||||
├── data/
|
||||
│ ├── training/ ← 4 החלטות לאימון (DOCX)
|
||||
│ ├── exports/ ← ייצוא legacy (תיקים ישנים)
|
||||
│ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB)
|
||||
├── web/ ← UI + API + integration clients
|
||||
├── mcp-server/ ← MCP server + services + tools
|
||||
└── scripts/ ← סקריפטים וכלי עזר
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ניהול משימות — TaskMaster AI
|
||||
|
||||
הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה:
|
||||
- **תמיד** להשתמש ב-TaskMaster לפירוק, מעקב וניהול משימות — לא ב-TASKS.md ידני
|
||||
- קובץ המשימות: `tasks/tasks.json`
|
||||
- פקודות עיקריות: `get_tasks`, `next_task`, `add_task`, `update_task`, `expand_task`
|
||||
- לפני התחלת עבודה → `next_task` כדי לדעת מה הבא לפי תלויות
|
||||
- אחרי סיום משימה → `update_task` עם status=done
|
||||
- משימה מורכבת → `expand_task` לפירוק לתתי-משימות
|
||||
|
||||
---
|
||||
|
||||
## עקרונות כתיבה קריטיים
|
||||
|
||||
1. **"מבחן השופט"** — כל החלטה חייבת להיות קריאה לשופט שלא מכיר את התיק
|
||||
2. **"רקע ניטרלי"** — בלוק ו = עובדות בלבד. אין ציטוטים מצדדים, אין מילות שיפוט
|
||||
3. **"ללא כפילות"** — בלוק י (דיון) מפנה לבלוקים קודמים, לא חוזר עליהם
|
||||
4. **"טענות מקוריות בלבד"** — בלוק ז = מכתבי טענות מקוריים בלבד. השלמות → בלוק ח
|
||||
5. **ארכיטקטורת 12 בלוקים** — ראה `docs/block-schema.md`
|
||||
6. **צ'קליסט תוכן** — בלוק י מקבל צ'קליסט תוכן אוטומטי לפי סוג הערר (ראה `lessons.py: CONTENT_CHECKLISTS`)
|
||||
|
||||
## הערות יו"ר (Chair Feedback)
|
||||
|
||||
מנגנון לתיעוד הערות דפנה על טיוטות:
|
||||
- **DB**: טבלת `chair_feedback` (case_id, block_id, feedback_text, category, lesson_extracted)
|
||||
- **API**: `GET/POST /api/feedback`, `PATCH /api/feedback/{id}/resolve`
|
||||
- **MCP tools**: `record_chair_feedback`, `list_chair_feedback`
|
||||
- **UI**: דף ניהול ב-`/feedback` (ב-Next.js)
|
||||
- **קטגוריות**: missing_content, wrong_tone, wrong_structure, factual_error, style, other
|
||||
|
||||
## יו"ר: עו"ד דפנה תמיר
|
||||
- מדריך סגנון מלא: `skills/decision/SKILL.md`
|
||||
|
||||
52
Dockerfile
52
Dockerfile
@@ -1,26 +1,40 @@
|
||||
FROM python:3.12-slim
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# Dockerfile — Next.js 16 web-ui (ui-rewrite branch only)
|
||||
#
|
||||
# This file REPLACES the FastAPI Dockerfile on this branch so that
|
||||
# Coolify's default /Dockerfile lookup builds the new Next.js staging
|
||||
# UI. The FastAPI Dockerfile lives on `main` and is unaffected.
|
||||
#
|
||||
# When the rewrite is merged to main, decide between:
|
||||
# (a) keeping both via separate Dockerfiles + dockerfile_location config, or
|
||||
# (b) a multi-stage Dockerfile that serves both, or
|
||||
# (c) fully replacing FastAPI's StaticFiles with this Next.js front end.
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY web-ui/package.json web-ui/package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
# System deps for PyMuPDF and document processing
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc libmupdf-dev libfreetype6-dev libharfbuzz-dev libjpeg62-turbo-dev \
|
||||
libopenjp2-7-dev curl && rm -rf /var/lib/apt/lists/*
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY web-ui/ ./
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
# Copy MCP server source (for importing services)
|
||||
COPY mcp-server/pyproject.toml /app/mcp-server/pyproject.toml
|
||||
COPY mcp-server/src/ /app/mcp-server/src/
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# Install MCP server dependencies + web deps
|
||||
RUN pip install --no-cache-dir /app/mcp-server && \
|
||||
pip install --no-cache-dir fastapi uvicorn python-multipart
|
||||
# next.config.ts uses output: 'standalone', so we copy only the minimal runtime
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
# Copy web app
|
||||
COPY web/ /app/web/
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PYTHONPATH=/app/mcp-server/src
|
||||
ENV DOTENV_PATH=/home/chaim/.env
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["uvicorn", "web.app:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
92
data/abbreviations.json
Normal file
92
data/abbreviations.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"legal": {
|
||||
"עוייד": "עו\"ד",
|
||||
"בייכ": "ב\"כ",
|
||||
"תבייע": "תב\"ע",
|
||||
"עייא": "ע\"א",
|
||||
"עייר": "ע\"ר",
|
||||
"בגייץ": "בג\"ץ",
|
||||
"עייב": "ע\"ב",
|
||||
"תייא": "ת\"א",
|
||||
"עייע": "ע\"ע",
|
||||
"סייח": "ס\"ח",
|
||||
"קיית": "ק\"ת",
|
||||
"פייד": "פ\"ד",
|
||||
"דייר": "ד\"ר",
|
||||
"תייד": "ת\"ד",
|
||||
"חייכ": "ח\"כ",
|
||||
"נייצ": "נ\"צ",
|
||||
"הייפ": "ה\"פ",
|
||||
"בשייא": "בש\"א",
|
||||
"עעייא": "עע\"א",
|
||||
"עעייר": "עע\"ר",
|
||||
"ברייע": "בר\"ע",
|
||||
"רעייא": "רע\"א",
|
||||
"עמייש": "עמ\"ש",
|
||||
"רשבייע": "רשב\"ע",
|
||||
"תמייא": "תמ\"א",
|
||||
"תמייל": "תמ\"ל",
|
||||
"תמיימ": "תמ\"מ",
|
||||
"נתבייע": "נתב\"ע",
|
||||
"עתמיי": "עתמ\"י",
|
||||
"חייפ": "ח\"פ",
|
||||
"עייח": "ע\"ח",
|
||||
"סייק": "ס\"ק",
|
||||
"הייד": "ה\"ד",
|
||||
"עייפ": "ע\"פ",
|
||||
"תייפ": "ת\"פ",
|
||||
"עייש": "ע\"ש",
|
||||
"בייש": "ב\"ש",
|
||||
"עררייב": "ערר\"ב",
|
||||
"עררייר": "ערר\"ר",
|
||||
"רמיי": "רמ\"י",
|
||||
"מחייק": "מח\"ק",
|
||||
"דנייא": "דנ\"א",
|
||||
"בריימ": "בר\"מ",
|
||||
"עייי": "ע\"י",
|
||||
"בייד": "ב\"ד",
|
||||
"בייה": "ב\"ה",
|
||||
"עההייש": "עהה\"ש",
|
||||
"החלייל": "החל\"ל",
|
||||
"ועההייש": "ועהה\"ש"
|
||||
},
|
||||
"general_hebrew": {
|
||||
"בסייד": "בס\"ד",
|
||||
"בעייה": "בע\"ה",
|
||||
"וכוי": "וכו'",
|
||||
"פרופי": "פרופ'",
|
||||
"ייפ": "י\"פ",
|
||||
"אייש": "א\"ש",
|
||||
"רחי": "רח'",
|
||||
"גבי": "גב'",
|
||||
"מייר": "מ\"ר",
|
||||
"קמייר": "קמ\"ר",
|
||||
"סמייכ": "סמ\"כ",
|
||||
"ראשייל": "ראש\"ל",
|
||||
"מנכייל": "מנכ\"ל",
|
||||
"יוייר": "יו\"ר",
|
||||
"מזכייל": "מזכ\"ל",
|
||||
"תייז": "ת\"ז",
|
||||
"שייח": "ש\"ח",
|
||||
"דוייח": "דו\"ח",
|
||||
"עייד": "ע\"ד",
|
||||
"אייא": "א\"א",
|
||||
"צהייל": "צה\"ל",
|
||||
"עייג": "ע\"ג",
|
||||
"עייס": "ע\"ס",
|
||||
"כדוייב": "כדו\"ב",
|
||||
"סמנכייל": "סמנכ\"ל"
|
||||
},
|
||||
"planning_specific": {
|
||||
"בניינייע": "בניין\"ע",
|
||||
"ועההייש": "ועהה\"ש",
|
||||
"ותייל": "ות\"ל",
|
||||
"הבייח": "הב\"ח",
|
||||
"תחבייצ": "תחב\"צ",
|
||||
"מבנייע": "מבנ\"ע",
|
||||
"ועדיימ": "ועד\"מ",
|
||||
"ועלייר": "ועל\"ר",
|
||||
"רשותיימ": "רשות\"מ",
|
||||
"ועתייב": "ועת\"ב"
|
||||
}
|
||||
}
|
||||
875
data/benchmark-embeddings.json
Normal file
875
data/benchmark-embeddings.json
Normal file
@@ -0,0 +1,875 @@
|
||||
{
|
||||
"voyage-3-large": {
|
||||
"doc_time": 2.160531520843506,
|
||||
"query_time": 0.3691830635070801,
|
||||
"doc_tokens": 29372,
|
||||
"query_tokens": 110,
|
||||
"total_tokens": 29482,
|
||||
"cost_usd": 0.00176892,
|
||||
"dimensions": 1024,
|
||||
"queries": [
|
||||
{
|
||||
"query": "מהי הטענה המרכזית של העוררים בנוגע לחניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.45552697235020545,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.43466572419373245,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.4198387036281709,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.39491882241110504,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.385910945433884,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה עמדת הוועדה המקומית לגבי התכנית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.4857051185745742,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.4774425600487735,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 0,
|
||||
"preview": "התקבל ב 02.09.25 **בפני ועדת ערר לתכנון ולבניה** **מחוז ירושלים** **העורר:** מרק"
|
||||
},
|
||||
{
|
||||
"score": 0.46114151010439974,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.4580968792031793,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 2,
|
||||
"preview": "הנ\"ל לטבלת המגרשים הריקים באזור הרווי ובכך החילו את הוראות תב\"ע 135 א' על מגרשים"
|
||||
},
|
||||
{
|
||||
"score": 0.4569195468700868,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 5,
|
||||
"preview": "\"לא לקחת הגדרה של זה שגם ככה הוציאו אותו בצורה יוצאת דופן... להפוך אותו שוב לעוד"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם יש פגיעה בזכויות הבנייה של השכנים?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5512569069784039,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 1,
|
||||
"preview": "קובע כי \"כל מעונין בקרקע, בבנין או בכל פרט תכנוני אחר הרואה את עצמו נפגע על ידי "
|
||||
},
|
||||
{
|
||||
"score": 0.5189215483366741,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 0,
|
||||
"preview": "בסייד בפני ועדת הערר לתכנון ובניה מחוז ירושלים מרק קובר ת.ז.21038994 מרחוב אבינד"
|
||||
},
|
||||
{
|
||||
"score": 0.4809795036642419,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
},
|
||||
{
|
||||
"score": 0.47633981918077806,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.47440112401616313,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 0,
|
||||
"preview": "ועדת ערר מחוז ירושלים 14. 08. 2025 נתקבל כתב ערר להחלטת הועדה המרחבית הראל לתכנו"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם התנאים שנקבעו בהיתר הבנייה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.3968560638750618,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 2,
|
||||
"preview": "198/09, שהנידון שם היה טרם הבניה, בנושא שגם הגדלת השטח וגם הוספת יחיד היו נחשבים"
|
||||
},
|
||||
{
|
||||
"score": 0.3870927920319327,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 3,
|
||||
"preview": "6291/95 בן יקר גת חברה להנדסה ובנין בע\"מ נ' הוועדה המיוחדת לתכנון ולבנייה מודיעי"
|
||||
},
|
||||
{
|
||||
"score": 0.38607617658065185,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 1,
|
||||
"preview": "הועדה המקומית לאשר גם תוספת יח\"ד וגם תוספת שטחים, כמבואר ב-62א(א)(8). ועדת הערר "
|
||||
},
|
||||
{
|
||||
"score": 0.3599807030961722,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.3581832424785113,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם התכנית עומדת בתקן החניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.4960205426989887,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.4753790492626444,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.46028934735585875,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.4534937167442651,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.4406330213463829,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה טענות המשיבים לגבי הגובה והצפיפות?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.3567138149575136,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.34847473216035035,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.34277808603786425,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.3426631344227079,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 0,
|
||||
"preview": "ועדת ערר מחוז ירושלים 14. 08. 2025 נתקבל כתב ערר להחלטת הועדה המרחבית הראל לתכנו"
|
||||
},
|
||||
{
|
||||
"score": 0.33710904804979946,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 0,
|
||||
"preview": "בסייד בפני ועדת הערר לתכנון ובניה מחוז ירושלים מרק קובר ת.ז.21038994 מרחוב אבינד"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם נערך שימוע כדין לפני מתן ההחלטה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.43099212823278577,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.3981475314512722,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
},
|
||||
{
|
||||
"score": 0.39788748681014513,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 2,
|
||||
"preview": "הנ\"ל לטבלת המגרשים הריקים באזור הרווי ובכך החילו את הוראות תב\"ע 135 א' על מגרשים"
|
||||
},
|
||||
{
|
||||
"score": 0.35426242747372305,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 2,
|
||||
"preview": "198/09, שהנידון שם היה טרם הבניה, בנושא שגם הגדלת השטח וגם הוספת יחיד היו נחשבים"
|
||||
},
|
||||
{
|
||||
"score": 0.35116758773177176,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 0,
|
||||
"preview": "# כתב ערר/תשובה - יצחק מטמון **תאריך:** 22.10.2025 **מגיש:** יצחק מטמון, אבינדב "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם הנימוקים לאישור התכנית על ידי הוועדה המקומית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.49844561506479357,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 0,
|
||||
"preview": "התקבל ב 02.09.25 **בפני ועדת ערר לתכנון ולבניה** **מחוז ירושלים** **העורר:** מרק"
|
||||
},
|
||||
{
|
||||
"score": 0.47658179941180195,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.46667693726943843,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.4636495882763162,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 1,
|
||||
"preview": "קובע כי \"כל מעונין בקרקע, בבנין או בכל פרט תכנוני אחר הרואה את עצמו נפגע על ידי "
|
||||
},
|
||||
{
|
||||
"score": 0.4618473840057438,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"voyage-4-large": {
|
||||
"doc_time": 0.7145340442657471,
|
||||
"query_time": 0.43074727058410645,
|
||||
"doc_tokens": 29372,
|
||||
"query_tokens": 110,
|
||||
"total_tokens": 29482,
|
||||
"cost_usd": 0.00353784,
|
||||
"dimensions": 1024,
|
||||
"queries": [
|
||||
{
|
||||
"query": "מהי הטענה המרכזית של העוררים בנוגע לחניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.4779343984469138,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.4286969964115333,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.4184225266072276,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.397857306516823,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 1,
|
||||
"preview": "בעיית משאית פינוי האשפה ופתרון בעיית החניות הקיימת – ללא תוספות חריגות בבניינים "
|
||||
},
|
||||
{
|
||||
"score": 0.3755498784052147,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה עמדת הוועדה המקומית לגבי התכנית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.48101631745565193,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.4805317589472102,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.45861226378349235,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 2,
|
||||
"preview": "הנ\"ל לטבלת המגרשים הריקים באזור הרווי ובכך החילו את הוראות תב\"ע 135 א' על מגרשים"
|
||||
},
|
||||
{
|
||||
"score": 0.45105895759375003,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 5,
|
||||
"preview": "\"לא לקחת הגדרה של זה שגם ככה הוציאו אותו בצורה יוצאת דופן... להפוך אותו שוב לעוד"
|
||||
},
|
||||
{
|
||||
"score": 0.4452113301852504,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם יש פגיעה בזכויות הבנייה של השכנים?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.47788408545232797,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.4728887051406596,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 1,
|
||||
"preview": "קובע כי \"כל מעונין בקרקע, בבנין או בכל פרט תכנוני אחר הרואה את עצמו נפגע על ידי "
|
||||
},
|
||||
{
|
||||
"score": 0.44009307393127606,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 0,
|
||||
"preview": "# כתב ערר/תשובה - יצחק מטמון **תאריך:** 22.10.2025 **מגיש:** יצחק מטמון, אבינדב "
|
||||
},
|
||||
{
|
||||
"score": 0.43846629246720314,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 0,
|
||||
"preview": "בסייד בפני ועדת הערר לתכנון ובניה מחוז ירושלים מרק קובר ת.ז.21038994 מרחוב אבינד"
|
||||
},
|
||||
{
|
||||
"score": 0.4296956323972593,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם התנאים שנקבעו בהיתר הבנייה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.39798937155509323,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.38511846982082976,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 1,
|
||||
"preview": "הועדה המקומית לאשר גם תוספת יח\"ד וגם תוספת שטחים, כמבואר ב-62א(א)(8). ועדת הערר "
|
||||
},
|
||||
{
|
||||
"score": 0.36698249480683875,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 2,
|
||||
"preview": "198/09, שהנידון שם היה טרם הבניה, בנושא שגם הגדלת השטח וגם הוספת יחיד היו נחשבים"
|
||||
},
|
||||
{
|
||||
"score": 0.36685990030225546,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.348382733103959,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם התכנית עומדת בתקן החניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.3948278938663396,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.3567420744746239,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.35597212422075997,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.3331851765322283,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.3326197383492352,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה טענות המשיבים לגבי הגובה והצפיפות?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.2799838857019288,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
},
|
||||
{
|
||||
"score": 0.27113472764757574,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.2586963050935262,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 0,
|
||||
"preview": "בסייד בפני ועדת הערר לתכנון ובניה מחוז ירושלים מרק קובר ת.ז.21038994 מרחוב אבינד"
|
||||
},
|
||||
{
|
||||
"score": 0.25151215229405505,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 0,
|
||||
"preview": "ועדת ערר מחוז ירושלים 14. 08. 2025 נתקבל כתב ערר להחלטת הועדה המרחבית הראל לתכנו"
|
||||
},
|
||||
{
|
||||
"score": 0.24453899390595563,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם נערך שימוע כדין לפני מתן ההחלטה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.3298147856975688,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.3081065688212409,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
},
|
||||
{
|
||||
"score": 0.30535746999969005,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
},
|
||||
{
|
||||
"score": 0.3012462701127593,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 2,
|
||||
"preview": "הנ\"ל לטבלת המגרשים הריקים באזור הרווי ובכך החילו את הוראות תב\"ע 135 א' על מגרשים"
|
||||
},
|
||||
{
|
||||
"score": 0.24831415939374535,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 0,
|
||||
"preview": "# כתב ערר/תשובה - יצחק מטמון **תאריך:** 22.10.2025 **מגיש:** יצחק מטמון, אבינדב "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם הנימוקים לאישור התכנית על ידי הוועדה המקומית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.455701010456977,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.4531137805769238,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 0,
|
||||
"preview": "התקבל ב 02.09.25 **בפני ועדת ערר לתכנון ולבניה** **מחוז ירושלים** **העורר:** מרק"
|
||||
},
|
||||
{
|
||||
"score": 0.44511363045803604,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.44510377110669735,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 5,
|
||||
"preview": "\"לא לקחת הגדרה של זה שגם ככה הוציאו אותו בצורה יוצאת דופן... להפוך אותו שוב לעוד"
|
||||
},
|
||||
{
|
||||
"score": 0.43812915786761897,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"voyage-law-2": {
|
||||
"doc_time": 11.868245124816895,
|
||||
"query_time": 5.83799147605896,
|
||||
"doc_tokens": 68508,
|
||||
"query_tokens": 311,
|
||||
"total_tokens": 68819,
|
||||
"cost_usd": 0.00825828,
|
||||
"dimensions": 1024,
|
||||
"queries": [
|
||||
{
|
||||
"query": "מהי הטענה המרכזית של העוררים בנוגע לחניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5719472051728132,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.5375637278159117,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.5325632912056516,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 1,
|
||||
"preview": "יח\"ד תוך מיצוי שטחי הבניה המותרים – מהווה ויתור היזם על** **היחידה השישית.** אלא"
|
||||
},
|
||||
{
|
||||
"score": 0.5258784945367275,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.5211251766446994,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה עמדת הוועדה המקומית לגבי התכנית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5914304292443499,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 6,
|
||||
"preview": "שלא היתה לוועדה המקומטת הסמכות לאישור התכנית, הן מפני שאישרו גם הוספת קומה וגם ה"
|
||||
},
|
||||
{
|
||||
"score": 0.5881555206233365,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.556911108120521,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
},
|
||||
{
|
||||
"score": 0.548469587955851,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.5478187405419974,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם יש פגיעה בזכויות הבנייה של השכנים?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.6490873939447226,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 6,
|
||||
"preview": "שלא היתה לוועדה המקומטת הסמכות לאישור התכנית, הן מפני שאישרו גם הוספת קומה וגם ה"
|
||||
},
|
||||
{
|
||||
"score": 0.6340108177311176,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 0,
|
||||
"preview": "בסייד בפני ועדת הערר לתכנון ובניה מחוז ירושלים מרק קובר ת.ז.21038994 מרחוב אבינד"
|
||||
},
|
||||
{
|
||||
"score": 0.6281689033296972,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
},
|
||||
{
|
||||
"score": 0.6262263506011073,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 3,
|
||||
"preview": "6291/95 בן יקר גת חברה להנדסה ובנין בע\"מ נ' הוועדה המיוחדת לתכנון ולבנייה מודיעי"
|
||||
},
|
||||
{
|
||||
"score": 0.6240746179234558,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 1,
|
||||
"preview": "קובע כי \"כל מעונין בקרקע, בבנין או בכל פרט תכנוני אחר הרואה את עצמו נפגע על ידי "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם התנאים שנקבעו בהיתר הבנייה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5946203725298453,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.5779431381169936,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 5,
|
||||
"preview": "\"לא לקחת הגדרה של זה שגם ככה הוציאו אותו בצורה יוצאת דופן... להפוך אותו שוב לעוד"
|
||||
},
|
||||
{
|
||||
"score": 0.5677818389824565,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 3,
|
||||
"preview": "6291/95 בן יקר גת חברה להנדסה ובנין בע\"מ נ' הוועדה המיוחדת לתכנון ולבנייה מודיעי"
|
||||
},
|
||||
{
|
||||
"score": 0.5642985608613257,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.5578101221696489,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 4,
|
||||
"preview": "העורר לעניין זה. ו. משמעות החלטת הועדה המחוזית משנת 2017. 6. יש לדחות את טענות ה"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם התכנית עומדת בתקן החניה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5641352335658906,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.5602931289883211,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 3,
|
||||
"preview": "בנייה. לשנן את הנקודה החשובה הזאת: לא אושרו 6 יח\"ד למגרש בווקום, אלא בתוך בניה מ"
|
||||
},
|
||||
{
|
||||
"score": 0.5430750080731945,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 6,
|
||||
"preview": "שלא היתה לוועדה המקומטת הסמכות לאישור התכנית, הן מפני שאישרו גם הוספת קומה וגם ה"
|
||||
},
|
||||
{
|
||||
"score": 0.5168002191989363,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 5,
|
||||
"preview": "מכך שכבר תוכנית מי/135א לפני כ-30 שנה אפשרה בניה רוויה במגרשים שטרם החלה הבניה, "
|
||||
},
|
||||
{
|
||||
"score": 0.5071732266091118,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מה טענות המשיבים לגבי הגובה והצפיפות?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.4992452616493536,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
},
|
||||
{
|
||||
"score": 0.48726688934334755,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.4319233887691947,
|
||||
"doc": "תשובת ועדת הראל",
|
||||
"chunk": 2,
|
||||
"preview": "בפרסום בין היתר גם בשים לב לעובדה שהעורר עצמו הגיש התנגדות ללמדך שהפרסום היה אפק"
|
||||
},
|
||||
{
|
||||
"score": 0.4185426925558015,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 5,
|
||||
"preview": "\"לא לקחת הגדרה של זה שגם ככה הוציאו אותו בצורה יוצאת דופן... להפוך אותו שוב לעוד"
|
||||
},
|
||||
{
|
||||
"score": 0.41545788222409435,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "האם נערך שימוע כדין לפני מתן ההחלטה?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.5928997875758119,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
},
|
||||
{
|
||||
"score": 0.5835634569931607,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 6,
|
||||
"preview": "שלא היתה לוועדה המקומטת הסמכות לאישור התכנית, הן מפני שאישרו גם הוספת קומה וגם ה"
|
||||
},
|
||||
{
|
||||
"score": 0.535693954454408,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 3,
|
||||
"preview": "לאחר פרסום ההחלטה, קיימו נציגי לשכת התכנון סיור ביישוב עם מהנדסת המועצה המקומית "
|
||||
},
|
||||
{
|
||||
"score": 0.5251227344556526,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
},
|
||||
{
|
||||
"score": 0.5010639755478099,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 5,
|
||||
"preview": "הועדה המחוזית. לכן, אין בנקודה הזאת של התנגדות רלוונטיות אלא אם כן כבוד ועדת הער"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"query": "מהם הנימוקים לאישור התכנית על ידי הוועדה המקומית?",
|
||||
"top5": [
|
||||
{
|
||||
"score": 0.608219402731344,
|
||||
"doc": "תשובת ליבמן",
|
||||
"chunk": 6,
|
||||
"preview": "המוצעת. 4. המשיבים הציגו פתרון חניה שקיבל אישור של יועץ התנועה של המועצה המקומית"
|
||||
},
|
||||
{
|
||||
"score": 0.5976481977620994,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 6,
|
||||
"preview": "שלא היתה לוועדה המקומטת הסמכות לאישור התכנית, הן מפני שאישרו גם הוספת קומה וגם ה"
|
||||
},
|
||||
{
|
||||
"score": 0.5605295780913448,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 6,
|
||||
"preview": "הנפגע למצות את זכותו להגיש התנגדות מנומקת ולטעון טענותיו בעל פה בפני הוועדה. ---"
|
||||
},
|
||||
{
|
||||
"score": 0.5398353352856547,
|
||||
"doc": "כתב ערר קובר",
|
||||
"chunk": 4,
|
||||
"preview": "למה ואיך, ואיפה האישור לבניין הקיים ל-5 יח\"ד מבלי אפילו מקום חניה אחד בתוך המגרש"
|
||||
},
|
||||
{
|
||||
"score": 0.5266016738343542,
|
||||
"doc": "כתב ערר מטמון",
|
||||
"chunk": 4,
|
||||
"preview": "חוות דעת משפטית, אך זו לא הוצגה, לא נידונה, ולא נמסרה לי. נאמר לי כי תוצג בדיון,"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
130
data/google-vision-extraction.json
Normal file
130
data/google-vision-extraction.json
Normal file
@@ -0,0 +1,130 @@
|
||||
[
|
||||
{
|
||||
"name": "1130-25-החלטה לתיקון פרוטוקול",
|
||||
"pages": 2,
|
||||
"chars": 2196,
|
||||
"words": 380,
|
||||
"time": 1.7020537853240967,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "1130-25-פרוטוקול ועדת ערר והחלטה",
|
||||
"pages": 16,
|
||||
"chars": 32246,
|
||||
"words": 6189,
|
||||
"time": 14.324745178222656,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "בקשה להשלמת טיעון ממשיבים 2-3",
|
||||
"pages": 1,
|
||||
"chars": 1179,
|
||||
"words": 191,
|
||||
"time": 0.8292603492736816,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "בקשת העורר לדחיית השלמת הטיעון במלואה",
|
||||
"pages": 12,
|
||||
"chars": 17267,
|
||||
"words": 3011,
|
||||
"time": 14.493695497512817,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "החלטת ביניים 1130-25",
|
||||
"pages": 2,
|
||||
"chars": 2221,
|
||||
"words": 389,
|
||||
"time": 1.4203259944915771,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "החלטת ועדה מקומית לאשר את התכנית",
|
||||
"pages": 1,
|
||||
"chars": 4074,
|
||||
"words": 718,
|
||||
"time": 0.8931229114532471,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "השלמת טיעון מטעם הוועדה המקומית",
|
||||
"pages": 3,
|
||||
"chars": 4658,
|
||||
"words": 809,
|
||||
"time": 2.1477737426757812,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "השלמת טיעון מטעם משיבים 2-3",
|
||||
"pages": 6,
|
||||
"chars": 6329,
|
||||
"words": 1080,
|
||||
"time": 5.7703633308410645,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "כתב תשובה-השלמת טיעון מטעם המשיב יצחק מטמון",
|
||||
"pages": 25,
|
||||
"chars": 49197,
|
||||
"words": 8404,
|
||||
"time": 25.85392999649048,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "מרק קובר-כתב ערר",
|
||||
"pages": 8,
|
||||
"chars": 17355,
|
||||
"words": 3101,
|
||||
"time": 0,
|
||||
"skipped": true
|
||||
},
|
||||
{
|
||||
"name": "פרוטוקול ועדה מקומית לדיון בתכנית 152-1257682",
|
||||
"pages": 7,
|
||||
"chars": 14373,
|
||||
"words": 2466,
|
||||
"time": 5.331800699234009,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "תגובת העורר לתשובת ועדת הראל להשלמת הטיעון ערר",
|
||||
"pages": 4,
|
||||
"chars": 8165,
|
||||
"words": 1427,
|
||||
"time": 3.2448556423187256,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "תשובה לערר מטעם המשיבים",
|
||||
"pages": 11,
|
||||
"chars": 17555,
|
||||
"words": 3072,
|
||||
"time": 0,
|
||||
"skipped": true
|
||||
},
|
||||
{
|
||||
"name": "תשובה מטעם העורר להשלמת טיעון",
|
||||
"pages": 3,
|
||||
"chars": 3388,
|
||||
"words": 615,
|
||||
"time": 2.0065605640411377,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "תשובת הועדה המרחבית לערר",
|
||||
"pages": 4,
|
||||
"chars": 6025,
|
||||
"words": 1084,
|
||||
"time": 3.2476346492767334,
|
||||
"skipped": false
|
||||
},
|
||||
{
|
||||
"name": "תשובת המשיב-יצחק מטמון",
|
||||
"pages": 19,
|
||||
"chars": 42415,
|
||||
"words": 7380,
|
||||
"time": 24.800947427749634,
|
||||
"skipped": false
|
||||
}
|
||||
]
|
||||
82
docs/architecture.md
Normal file
82
docs/architecture.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# System Architecture — Legal Decision Assistant
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Nautilus Server │
|
||||
│ 158.178.131.193 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ Coolify │ │ Traefik │ │ ezer-mishpati-web│ │
|
||||
│ │ (manage) │ │ (proxy) │ │ (upload UI) │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ PostgreSQL │ │ Redis │ │
|
||||
│ │ + pgvector │ │ (task queue) │ │
|
||||
│ │ (legal-ai-postgres│ │ (legal-ai-redis) │ │
|
||||
│ └──────────────────┘ └──────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Gitea │ │ n8n │ │
|
||||
│ │ (code) │ │ (automate│ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Claude Code (via SSH or API) │ │
|
||||
│ │ — Skills: legal-decision, legal-docx │ │
|
||||
│ │ — MCP: postgres, n8n, cloudflare, chrome │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
External:
|
||||
← Claude API (embeddings, analysis)
|
||||
← Cloudflare DNS (*.nautilus.marcusgroup.org)
|
||||
← User (Putty SSH / Browser)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
1. Document Upload
|
||||
User → ezer-mishpati-web → file storage → n8n trigger
|
||||
→ classify document → store metadata in PostgreSQL
|
||||
→ generate embeddings → store in pgvector
|
||||
|
||||
2. Decision Writing
|
||||
Claude Code → read source materials from DB
|
||||
→ generate structure DOCX (12 blocks)
|
||||
→ write each block with appropriate model/parameters
|
||||
→ validate against block-schema
|
||||
→ export final DOCX
|
||||
|
||||
3. Precedent Search (RAG)
|
||||
Query → generate embedding → pgvector similarity search
|
||||
→ return relevant paragraphs/decisions
|
||||
→ Claude analyzes relevance → present to user
|
||||
```
|
||||
|
||||
## Database Schema — 4 Layers
|
||||
|
||||
### Layer 1: Core
|
||||
appeals, parties, panels, documents
|
||||
|
||||
### Layer 2: Decision
|
||||
decisions, decision_blocks, decision_paragraphs, claims
|
||||
|
||||
### Layer 3: Legal Knowledge
|
||||
case_law, case_law_citations, statutory_provisions, transition_phrases, lessons_learned
|
||||
|
||||
### Layer 4: Semantic Search (RAG)
|
||||
document_embeddings, paragraph_embeddings, case_law_embeddings
|
||||
(all using pgvector vector(1536) columns)
|
||||
|
||||
## Technology Choices
|
||||
- **Database**: PostgreSQL 15 + pgvector 0.8.1
|
||||
- **Embedding model**: TBD (Claude/OpenAI ada-002/local)
|
||||
- **Automation**: n8n (workflow engine)
|
||||
- **Code repository**: Gitea (self-hosted)
|
||||
- **Deployment**: Coolify (Docker management)
|
||||
- **Proxy**: Traefik v3.6 (auto-SSL)
|
||||
- **Frontend**: ezer-mishpati-web (static HTML + API)
|
||||
125
docs/audit-report.md
Normal file
125
docs/audit-report.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# דוח ביקורת תיקים — עוזר משפטי
|
||||
**תאריך:** 2 באפריל 2026
|
||||
**סורק:** Claude Code + סריקה אוטומטית של legacy vault
|
||||
|
||||
---
|
||||
|
||||
## סיכום כללי
|
||||
|
||||
| נתון | ערך |
|
||||
|------|-----|
|
||||
| סה"כ תיקים | 19 |
|
||||
| תיקים בארכיון | 16 |
|
||||
| תיקים פעילים | 3 |
|
||||
| סה"כ קבצים (ארכיון) | 319~ |
|
||||
| סה"כ קבצים (פעילים) | 125~ |
|
||||
| תיקים עם החלטה סופית | 6 |
|
||||
| תיקים עם טיוטה בלבד | 5 |
|
||||
| תיקים ללא החלטה | 8 |
|
||||
|
||||
---
|
||||
|
||||
## סטטוס תיקים
|
||||
|
||||
### תיקים עם החלטה סופית (6)
|
||||
|
||||
| תיק | מספר | סוג | תוצאה | קבצים |
|
||||
|-----|------|-----|-------|-------|
|
||||
| הכט | 1180-1181 | רישוי | דחייה | 12 |
|
||||
| אפרים אבי | 8255-25 | היטל השבחה | דחייה | 17 |
|
||||
| עומר דרוויש | 8007-24 | היטל השבחה | — | 17 |
|
||||
| אייל מבורך | 1113/25 | רישוי | — | 18 |
|
||||
| שטרית | 1128/25 | רישוי | — | 26 |
|
||||
| קרית יערים-2 | 1194/25+1199/25 | רישוי | — | 32 |
|
||||
|
||||
### תיקים עם טיוטה בלבד (5)
|
||||
|
||||
| תיק | מספר | סוג | הערות | קבצים |
|
||||
|-----|------|-----|-------|-------|
|
||||
| בית הכרם | 1126/25+1141/25 | תמ"א 38 | טיוטה 9 (סופית למעשה) | 31 |
|
||||
| אזורים | 8141-23 | היטל השבחה | טיוטה | 8 |
|
||||
| אבו זאהריה | 8107-25 | היטל השבחה | טיוטה | 32 |
|
||||
| רמת שלמה | 9005-24 | פיצויים ס' 197 | טיוטות | 35 |
|
||||
| קרית יערים-1 | 1130/25 | רישוי | טיוטת מבנה | 70+ |
|
||||
|
||||
### תיקים ללא החלטה (8)
|
||||
|
||||
| תיק | מספר | סוג | קבצים |
|
||||
|-----|------|-----|-------|
|
||||
| משכן אליהו | 8047-24 | היטל השבחה | 15 |
|
||||
| ערר 8070-25 | 8070-25 | היטל השבחה | 24 |
|
||||
| מרפסות שירות | 8136-24 | היטל השבחה | 13 |
|
||||
| רישוי 1184-25 | 1184/25 | רישוי | 8 |
|
||||
| ערר 1195-25 | 1195-25 | רישוי | 6 |
|
||||
| ערר 1200-25 | 1200/25 | רישוי | 6 |
|
||||
| בלוי | 1107/06/25 | תמ"א 38 | 25 |
|
||||
| תחכמוני | 8027-25 | היטל השבחה | 23 |
|
||||
|
||||
---
|
||||
|
||||
## התפלגות לפי סוג ערר
|
||||
|
||||
| סוג | כמות | עם החלטה | ללא החלטה |
|
||||
|-----|------|---------|----------|
|
||||
| רישוי ובנייה (1xxx) | 9 | 4 | 5 |
|
||||
| היטל השבחה (8xxx) | 8 | 2 | 6 |
|
||||
| תמ"א 38 | 2 | 0 | 2 |
|
||||
| פיצויים ס' 197 (9xxx) | 1 | 0 | 1 |
|
||||
|
||||
---
|
||||
|
||||
## התפלגות סוגי מסמכים (ארכיון)
|
||||
|
||||
| סוג מסמך | כמות | הערות |
|
||||
|----------|------|-------|
|
||||
| החלטות (סופיות + ביניים + טיוטות) | 70~ | כולל כל הגרסאות |
|
||||
| כתבי ערר | 49~ | חלקם כפולים (PDF + MD) |
|
||||
| כתבי תשובה / תגובות | 47~ | כולל השלמות טיעון |
|
||||
| פרוטוקולים ותמלולים | 23~ | ועדה מקומית + ערר |
|
||||
| שומות | 17~ | בעיקר תיקי היטל השבחה |
|
||||
| חוות דעת מומחים | 5~ | אדריכל, מהנדס, רעש |
|
||||
| תכניות | 1~ | — |
|
||||
| הגשות | 2~ | — |
|
||||
| אחר (ניתוחים, קטלוגים, MD) | 106~ | — |
|
||||
|
||||
---
|
||||
|
||||
## פערים שזוהו
|
||||
|
||||
### פערים קריטיים
|
||||
1. **בית הכרם** — מסווג כ"טיוטה" אבל למעשה טיוטה 9 היא הגרסה הסופית. צריך לעדכן סטטוס ל-final
|
||||
2. **קרית יערים-2** — יש החלטה מוגמרת ב-01_Projects אבל לא ב-04_Archive. צריך לוודא שזו הגרסה הסופית
|
||||
|
||||
### פערים במטאדטה
|
||||
3. **הכט, הכרם** — חסרים שמות צדדים ב-DB (appellants/respondents ריקים)
|
||||
4. **8 תיקים** — חסרים תאריכי דיון (hearing_date) ותאריכי החלטה (decision_date)
|
||||
5. **כל התיקים** — חסר permit_number
|
||||
|
||||
### כפילויות
|
||||
6. **רוב המסמכים קיימים בשני פורמטים** — PDF + MD. ה-MD הוא טקסט מחולץ. עדיף לייבא את שני הפורמטים: PDF כקובץ מקורי, MD כטקסט מחולץ מוכן
|
||||
|
||||
### מסמכים גדולים (דורשים תשומת לב ב-OCR)
|
||||
7. כתב ערר אזורים 8141-23 — 9.2MB
|
||||
8. כתב ערר מבורך 1113-25 — 9.3MB
|
||||
9. כתב ערר קובר (קרית יערים-1) — 8.6MB
|
||||
10. חוות דעת אדריכל רמת שלמה — 7.4MB
|
||||
|
||||
---
|
||||
|
||||
## המלצות לשלב הבא
|
||||
|
||||
### עדיפות ראשונה — תיקים עם החלטה סופית
|
||||
לייבא קודם את 6 התיקים עם החלטות סופיות + בית הכרם (טיוטה 9):
|
||||
1. הכט 1180-1181
|
||||
2. בית הכרם 1126/25
|
||||
3. אפרים אבי 8255-25
|
||||
4. עומר דרוויש 8007-24
|
||||
5. אייל מבורך 1113/25
|
||||
6. שטרית 1128/25
|
||||
7. קרית יערים-2 1194/25+1199/25
|
||||
|
||||
### עדיפות שנייה — ניצול קבצי MD
|
||||
רוב המסמכים כבר מחולצים ל-Markdown. זה חוסך OCR — אפשר לייבא את ה-MD ישירות כ-extracted_text
|
||||
|
||||
### עדיפות שלישית — עדכון מטאדטה
|
||||
להשלים שמות צדדים, תאריכים, ומספרי היתר לכל 19 התיקים
|
||||
575
docs/block-schema.md
Normal file
575
docs/block-schema.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# Block Schema — ארכיטקטורת מסמך החלטת ועדת ערר
|
||||
|
||||
מסמך זה מגדיר את המבנה הפורמלי של החלטת ועדת ערר לתכנון ובניה. הוא משמש כמקור סמכותי להגדרת בלוקים, משקלות, פרמטרי עיבוד, וכללי ולידציה.
|
||||
|
||||
**הפניה:** SKILL.md סעיפים 11-12 מכילים סיכום מהיר והנחיות תהליך. מסמך זה מכיל את ההגדרות המלאות.
|
||||
|
||||
---
|
||||
|
||||
## 1. יסודות תיאורטיים
|
||||
|
||||
ארכיטקטורת המסמך מבוססת על שילוב של ארבעה frameworks מוכרים:
|
||||
|
||||
### CREAC — מתודולוגיית כתיבה משפטית
|
||||
Conclusion → Rule → Explanation → Application → Conclusion.
|
||||
מקור: Columbia Law School, Legal Writing methodology.
|
||||
**מיפוי:** חל על בלוק י (דיון) ובלוק יא (סיכום). בלוק י פותח במסקנה (C), מציג כלל משפטי (R), מסביר באמצעות פסיקה (E), מיישם על העובדות (A), וחוזר למסקנה (C). בלוק יא = C אחרון בלבד.
|
||||
|
||||
### Federal Judicial Center — Judicial Writing Manual
|
||||
מגדיר תפקוד פונקציונלי לכל חלק בהחלטה שיפוטית:
|
||||
- **Orientation** (אוריינטציה) — מי, מה, איפה → בלוקים א-ה
|
||||
- **Framing** (מסגור) — הקשר עובדתי ותכנוני → בלוק ו
|
||||
- **Argumentation** (טיעון) — עמדות הצדדים → בלוק ז
|
||||
- **Procedural record** (תיעוד הליכי) — מה עשינו → בלוק ח
|
||||
- **Deliberation** (דיון) — ניתוח משפטי → בלוקים ט-י
|
||||
- **Disposition** (החלטה) — תוצאה אופרטיבית → בלוק יא
|
||||
|
||||
### DITA — Darwin Information Typing Architecture
|
||||
סטנדרט OASIS להגדרת סוגי תוכן מובנים. מספק:
|
||||
- **Content model** — אילו אלמנטים מותרים בכל בלוק
|
||||
- **Constraints** — מה אסור (חשוב יותר ממה שמותר)
|
||||
- **Specialization** — ירושה מסוג בסיסי עם התאמות
|
||||
- **Relationships** — תלויות בין בלוקים
|
||||
|
||||
### Akoma Ntoso / LegalDocumentML
|
||||
סטנדרט OASIS בינלאומי למסמכים משפטיים מובנים (UN/DESA). מספק:
|
||||
- **Semantic mapping** — כל בלוק ממופה לרכיב מוכר בסטנדרט
|
||||
- **Document class** — "judgment" (פסק דין / החלטה)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. הגדרות בלוקים
|
||||
|
||||
### Block א: כותרת מוסדית / Institutional Header
|
||||
|
||||
**ID:** `block-alef`
|
||||
**Akoma Ntoso:** `meta > identification`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את המוסד, התיק והגורם המחליט.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: טבלה 2 טורים (מוסד | מספרי תיק)
|
||||
- Sources: מערכת ניהול תיקים
|
||||
|
||||
**Constraints:**
|
||||
- MUST: שם מוסד, מספר תיק, מספר תכנית/בקשה
|
||||
- MUST NOT: תוכן מהותי כלשהו
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע, לא משתנה בין סוגי עררים)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ב: הרכב הוועדה / Panel Composition
|
||||
|
||||
**ID:** `block-bet`
|
||||
**Akoma Ntoso:** `meta > references > TLCPerson`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את ההרכב המחליט. חשוב לביקורת שיפוטית (הרכב כשיר).
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: "בפני:" + יו"ר + חברים
|
||||
- Sources: מערכת ניהול
|
||||
|
||||
**Constraints:**
|
||||
- MUST: יו"ר + לפחות חבר אחד
|
||||
- MUST NOT: תוכן מהותי
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ג: צדדים / Parties
|
||||
|
||||
**ID:** `block-gimel`
|
||||
**Akoma Ntoso:** `meta > references > TLCPerson` (appellants, respondents)
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את הצדדים וב"כ. מגדיר את מסגרת הדיון.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: עוררים + "נגד" + משיבים + ב"כ
|
||||
- Sources: כתב ערר, כתב תשובה
|
||||
|
||||
**Constraints:**
|
||||
- MUST: שם כל צד, "נגד" כמפריד
|
||||
- MUST NOT: תוכן מהותי, תיאור הערר
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ד: כותרת "החלטה" / Decision Title
|
||||
|
||||
**ID:** `block-dalet`
|
||||
**Akoma Ntoso:** `body > judgment > header`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — סימון פורמלי של תחילת ההחלטה.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: מילה אחת: "החלטה"
|
||||
- Sources: none
|
||||
|
||||
**Constraints:**
|
||||
- MUST: David 16pt, bold, מרכז
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 0% (שורה אחת)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ה: פתיחה / Opening
|
||||
|
||||
**ID:** `block-he`
|
||||
**Akoma Ntoso:** `body > judgment > introduction`
|
||||
**CREAC role:** C (מסקנה ראשונית — הצגת מה לפנינו)
|
||||
**Functional purpose (JWM):** Orientation — מכוון את הקורא למהות הערר במשפט אחד. מגדיר "להלן" מרכזיים.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative (1-2 סעיפים)
|
||||
- Elements: numbered-para עם הגדרות "להלן"
|
||||
- Sources: כתב ערר, החלטת ועדה מקומית
|
||||
|
||||
**Constraints:**
|
||||
- MUST: "לפנינו...", הגדרת הוועדה המקומית, הגדרת התכנית/הבקשה, הגדרת המגרש
|
||||
- MUST NOT: ניתוח, ערכי שיפוט, ציטוטים מצדדים
|
||||
- Dependencies: block-gimel (שמות צדדים להגדרות)
|
||||
|
||||
**Weight:** 1% (קבוע — 1-2 סעיפים)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase
|
||||
- Temperature: 0.2 | Thinking: low | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ו: רקע עובדתי / Factual Background ("פתח דבר")
|
||||
|
||||
**ID:** `block-vav`
|
||||
**Akoma Ntoso:** `body > judgment > background`
|
||||
**CREAC role:** none (עובדות בלבד, לא ניתוח)
|
||||
**Functional purpose (JWM):** Framing — מספק את התשתית העובדתית שעליה נבנה הדיון. השופט חייב להבין את המציאות בשטח לפני שקורא טענות.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block, image-placeholder
|
||||
- Elements: numbered-para, blockquote (ציטוט מפרוטוקול), image-box
|
||||
- Sources: כתבי טענות, תשריטים, פרוטוקולים, החלטות קודמות, GIS
|
||||
|
||||
**סדר תוכן פנימי:**
|
||||
1. מקרקעין — מיקום, שטח, מאפיינים
|
||||
2. סביבת מקרקעין — בנייה סמוכה, אופי
|
||||
3. 📷 תמונה: מיקום GIS
|
||||
4. היסטוריה תכנונית — תכניות, החלטות (עובדות יבשות בלבד)
|
||||
5. מהות הבקשה/תכנית
|
||||
6. 📷 תמונה: תשריט
|
||||
7. ציטוט מפרוטוקול ועדה מקומית
|
||||
8. החלטת הוועדה + תנאים
|
||||
9. 📷 תמונה: צילום אוויר (אופציונלי)
|
||||
10. הגשת הערר
|
||||
|
||||
**Constraints:**
|
||||
- MUST: מקרקעין, מהות הבקשה, החלטת הוועדה, הגשת הערר
|
||||
- MUST: לפחות 2 תמונות (מיקום + תשריט)
|
||||
- MUST: ציטוט מפרוטוקול הוועדה המקומית
|
||||
- ⚠️ **MUST NOT ("רקע ניטרלי"):** ציטוטים ישירים מצדדים, מילות ערך/שיפוט ("חריג", "חטא", "בעייתי"). החלטות קודמות = עובדה יבשה ("ביום X נדחתה תכנית Y"), ללא נימוקים וציטוטים מהן.
|
||||
- Dependencies: block-he (הגדרות "להלן")
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 15-25% | רקע מפורט עם הקשר תכנוני |
|
||||
| רישוי — קבלה | 30-40% | כולל ציטוט מפרוטוקול |
|
||||
| רישוי — קבלה חלקית | 25-35% | כולל ציטוט מפרוטוקול |
|
||||
| היטל השבחה | 6-18% | רקע מצומצם |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): גבוה — מספק את "התמונה" לשופט שלא מכיר את התיק
|
||||
- Reader attention (20%): בינוני-גבוה — primacy effect, הקורא קשוב בהתחלה
|
||||
- Judicial review (25%): גבוה — שופט בודק שהעובדות מלאות ומדויקות
|
||||
- Empirical (15%): מבוסס על מדידת החלטות דפנה (3.2 ב-SKILL.md)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: reproduction (העתקה נאמנה ממקורות)
|
||||
- Cognitive complexity: lookup (ארגון, לא ניתוח)
|
||||
- Accuracy: high-precision
|
||||
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ז: טענות הצדדים / Parties' Claims
|
||||
|
||||
**ID:** `block-zayin`
|
||||
**Akoma Ntoso:** `body > judgment > arguments`
|
||||
**CREAC role:** none (הצגת טענות, לא ניתוח)
|
||||
**Functional purpose (JWM):** Argumentation — מציג את עמדות הצדדים בנאמנות, כך שהקורא יבין את המחלוקת לפני שקורא את ההכרעה.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative
|
||||
- Elements: section-heading ("תמצית טענות הצדדים"), sub-headings (לכל צד), numbered-para
|
||||
- Sources: כתב ערר, כתב תשובה — **כתבי טענות מקוריים בלבד** (לא השלמות טיעון)
|
||||
|
||||
**סדר קבוע:**
|
||||
1. כותרת: "תמצית טענות הצדדים"
|
||||
2. "טענות העוררים" (אם כמה עוררים — תתי-כותרות לכל אחד)
|
||||
3. "עמדת הוועדה המקומית"
|
||||
4. "עמדת מבקשי ההיתר" / "עמדת מגישי התכנית"
|
||||
|
||||
**Constraints:**
|
||||
- MUST: כל טענה בסעיף נפרד, גוף שלישי ("העורר טוען כי...")
|
||||
- MUST: כל צד בפרק נפרד, סדר קבוע
|
||||
- MUST NOT: ניתוח, מסקנות, הערכת הוועדה ("טענה זו חלשה...")
|
||||
- MUST NOT: תוכן מהשלמות טיעון (→ block-chet)
|
||||
- Dependencies: block-vav (מספור רציף)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 30-40% | טענות מפורטות |
|
||||
| רישוי — קבלה | 20-30% | כולל השלמות |
|
||||
| רישוי — קבלה חלקית | 25-30% | |
|
||||
| היטל השבחה | 13-25% | |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): בינוני — הצגה, לא הכרעה
|
||||
- Reader attention (20%): נמוך-בינוני — scanning attention, הקורא מחפש טענות ספציפיות
|
||||
- Judicial review (25%): גבוה — שופט בודק ש"נשמעו כל הצדדים"
|
||||
- Empirical (15%): מבוסס על מדידת החלטות דפנה
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase (סיכום נאמן בשפה של דפנה)
|
||||
- Cognitive complexity: medium-synthesis (קיבוץ וסידור טענות)
|
||||
- Accuracy: high-precision (לא לפספס טענה, לא לעוות)
|
||||
- Temperature: 0.1 | Thinking: low | Effort: medium | Model: sonnet
|
||||
|
||||
|
||||
### Block ח: הליכים בפני ועדת הערר / Proceedings
|
||||
|
||||
**ID:** `block-chet`
|
||||
**Akoma Ntoso:** `body > judgment > proceedings` (custom extension)
|
||||
**CREAC role:** none (תיעוד, לא ניתוח)
|
||||
**Functional purpose (JWM):** Procedural record — מתעד שהוועדה פעלה כדין ונתנה מלוא יום בבית דין. קריטי ל"מבחן השופט" — שופט בעתמ"ם בודק שהצדדים קיבלו הזדמנות הוגנת.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, image-placeholder
|
||||
- Elements: section-heading ("ההליכים בפני ועדת הערר"), numbered-para, image-box
|
||||
- Sources: פרוטוקול דיון, תמונות סיור, החלטות ביניים, השלמות טיעון
|
||||
|
||||
**סדר כרונולוגי:**
|
||||
1. דיון — תאריך, נוכחים
|
||||
2. סיור — תאריך, תיאור
|
||||
3. 📷 תמונה: צילומים מהסיור
|
||||
4. השלמות טיעון — עם תוכן מפורט (כל השלמה = סעיף נפרד)
|
||||
5. החלטות ביניים
|
||||
6. תגובות לתגובות — כרונולוגי
|
||||
7. 📷 תמונה: הדמיות/חתכים (אם צורפו)
|
||||
8. עררים מקבילים (אם יש)
|
||||
|
||||
**Constraints:**
|
||||
- MUST: תאריכים מדויקים, כרונולוגיה ברורה
|
||||
- MUST: תוכן השלמות טיעון מפורט — כל השלמה בסעיף נפרד עם תמצית תוכן
|
||||
- MUST NOT: ניתוח או הערכה של ההשלמות ("טענה חזקה/חלשה")
|
||||
- Dependencies: block-zayin (מספור רציף)
|
||||
- References: block-zayin (הפניה לטענות מקוריות כשיש חפיפה)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| ערר פשוט (ללא השלמות) | 3-5% | דיון + סיור בלבד |
|
||||
| ערר מורכב (השלמות רבות) | 8-15% | כמו אריאלי: 31 סעיפים |
|
||||
| היטל השבחה | 2-4% | בדרך כלל מינימלי |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): נמוך-בינוני — תיעוד, לא הכרעה
|
||||
- Reader attention (20%): נמוך — scanning, אלא אם יש ממצאים חדשים מסיור/השלמות
|
||||
- Judicial review (25%): **גבוה מאוד** — שופט בודק שנתנו procedural fairness
|
||||
- Empirical (15%): מגוון רחב — תלוי בכמות ההשלמות
|
||||
|
||||
**Processing:**
|
||||
- Generation type: reproduction + paraphrase (תאריכים מדויקים + תמצית תוכן)
|
||||
- Cognitive complexity: low (סידור כרונולוגי)
|
||||
- Accuracy: high-precision (תאריכים, שמות מסמכים)
|
||||
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ט: תכניות חלות / Applicable Plans (אופציונלי)
|
||||
|
||||
**ID:** `block-tet`
|
||||
**Akoma Ntoso:** `body > judgment > motivation > background` (extended)
|
||||
**CREAC role:** R (Rule — הצגת הכללים המשפטיים/תכנוניים)
|
||||
**Functional purpose (JWM):** Deliberation (preliminary) — מציג את המסגרת הנורמטיבית שלאורה ייבחנו הטענות. בלוק גשר בין עובדות לניתוח.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block
|
||||
- Elements: section-heading, numbered-para, blockquote (ציטוט מהוראות תכנית)
|
||||
- Sources: הוראות תכנית (PDF), נספחי בינוי, החלטות מרכזות
|
||||
|
||||
**Constraints:**
|
||||
- MUST: ציטוט ישיר מהוראות תכנית עם הדגשת (bold) מילים מכריעות
|
||||
- MUST NOT: ניתוח מעמיק (→ block-yod), הכרעה בין פרשנויות
|
||||
- Dependencies: block-chet (מספור), block-vav (הגדרות תכניות)
|
||||
- Condition: **אופציונלי** — רק כשיש מורכבות תכנונית (תכניות סותרות, תמ"א 38 + שימור, פרשנות)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| מתי קיים | משקל |
|
||||
|----------|------|
|
||||
| תמ"א 38 + שימור | 8-12% |
|
||||
| פרשנות תכנית | 5-10% |
|
||||
| לא קיים | 0% |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): בינוני — הנחת תשתית נורמטיבית
|
||||
- Reader attention (20%): נמוך — טכני, אלא אם פרשנות שנויה במחלוקת
|
||||
- Judicial review (25%): בינוני — שופט בודק שהוועדה הבינה את הדין
|
||||
- Empirical (15%): אריאלי — 14 סעיפים; בית הכרם — משולב בדיון
|
||||
|
||||
**Processing:**
|
||||
- Generation type: guided-synthesis (ציטוט + ניתוח ראשוני)
|
||||
- Cognitive complexity: medium (פרשנות טקסט משפטי)
|
||||
- Accuracy: precision + interpretation
|
||||
- Temperature: 0.2 | Thinking: medium | Effort: medium | Model: opus
|
||||
|
||||
|
||||
### Block י: דיון והכרעה / Discussion and Decision
|
||||
|
||||
**ID:** `block-yod`
|
||||
**Akoma Ntoso:** `body > judgment > motivation`
|
||||
**CREAC role:** **full-CREAC** — C (מסקנה בפתיחה) → R (כלל משפטי) → E (ציטוט פסיקה) → A (יישום על העובדות) → C (מסקנת ביניים)
|
||||
**Functional purpose (JWM):** Deliberation — ליבת ההחלטה. כאן הוועדה מנתחת, מאזנת, ומכריעה. זהו ה-ratio decidendi.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block, image-placeholder
|
||||
- Elements: numbered-para (אסה רציפה ללא כותרות משנה), blockquote (ציטוטי פסיקה ותכנית), image-box
|
||||
- Sources: **כל** הבלוקים הקודמים + פסיקה + skill
|
||||
|
||||
**מבנה פנימי (לפי סוג ערר — ראה SKILL.md סעיף 7.3):**
|
||||
- דחייה: שכבות הגנה (concentric circles)
|
||||
- קבלה: נימוק-נימוק
|
||||
- קבלה חלקית: מיפוי מתחים + ניתוח נושאי
|
||||
- היטל השבחה: פתיחה ישירה עם מסקנה
|
||||
|
||||
**Constraints:**
|
||||
- MUST: מסקנה בפתיחת הדיון (לא בסוף)
|
||||
- MUST: מענה לכל טענה שהוצגה בבלוק ז
|
||||
- MUST: ציטוט פסיקה בבלוקים ארוכים (200-600 מילים)
|
||||
- MUST: **צ'קליסט תוכן** — הפרומפט מזריק `{content_checklist}` אוטומטית לפי סוג הערר (מתוך `lessons.py: CONTENT_CHECKLISTS`). ראה `docs/corpus-analysis.md` לדפוסי תוכן לפי סוג.
|
||||
- ⚠️ **MUST NOT ("ללא כפילות"):** חזרה על עובדות/טענות מבלוקים קודמים. השתמש בהפניות: "כאמור בסעיף X לעיל", "כפי שפורט", "כפי שציינו"
|
||||
- MUST NOT: כותרות משנה (חריג: נושאים נפרדים לחלוטין)
|
||||
- Dependencies: **ALL** previous blocks (ה-ט)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 37-50% | פתיחה רחבה + שכבות |
|
||||
| רישוי — קבלה | 35-45% | נימוק-נימוק |
|
||||
| רישוי — קבלה חלקית | 40-47% | מיפוי מתחים + ניתוח נושאי |
|
||||
| היטל השבחה | 32-48% | ציטוטי פסיקה מרובים |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): **מקסימלי** — זהו ה-ratio decidendi, תכלית ההחלטה
|
||||
- Reader attention (20%): **גבוה** — deep reading, הקורא מחפש את הנימוקים
|
||||
- Judicial review (25%): **מקסימלי** — שופט בוחן סבירות, מידתיות, התייחסות לטענות
|
||||
- Empirical (15%): 35-50% באופן עקבי בכל החלטות דפנה
|
||||
|
||||
**Processing:**
|
||||
- Generation type: **rhetorical-construction** (בניית טיעון, איזון, רטוריקה)
|
||||
- Cognitive complexity: **high-reasoning** (CREAC מלא, שכבות, חידוד)
|
||||
- Accuracy: **precision + creativity** (ניתוח מדויק + ביטוי אלגנטי)
|
||||
- Temperature: **0.4** | Thinking: **max (budget 16K+)** | Effort: **max** | Model: **opus בלבד**
|
||||
|
||||
|
||||
### Block יא: סיכום / סוף דבר / Summary
|
||||
|
||||
**ID:** `block-yod-alef`
|
||||
**Akoma Ntoso:** `body > judgment > decision`
|
||||
**CREAC role:** C (Conclusion אחרון — תמצית אופרטיבית)
|
||||
**Functional purpose (JWM):** Disposition — ההוראה האופרטיבית שמבצעים. זה מה שהצדדים צריכים לדעת "מה עכשיו."
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative
|
||||
- Elements: section-heading ("סיכום"/"סוף דבר"), numbered-para, sub-items (א. ב. ג.)
|
||||
- Sources: block-yod (מסקנות)
|
||||
|
||||
**מבנה לפי תוצאה (ראה SKILL.md סעיף 8):**
|
||||
- דחייה: "הערר נדחה" + תתי-סעיפים + פסקה חמה (רישוי בלבד)
|
||||
- קבלה: "הערר מתקבל בכפוף ל..." + פרוזה
|
||||
- קבלה חלקית: "הערר מתקבל באופן חלקי" + 2-3 הוראות אופרטיביות
|
||||
- היטל השבחה: יבש
|
||||
|
||||
**Constraints:**
|
||||
- MUST: תוצאה ברורה (נדחה/מתקבל/מתקבל חלקית)
|
||||
- MUST NOT (בקבלה חלקית): חזרה על נימוקים — ההנמקה כבר בדיון
|
||||
- Dependencies: block-yod (מסקנות)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל |
|
||||
|---------|------|
|
||||
| דחייה | 2-9% |
|
||||
| קבלה | 3-5% |
|
||||
| קבלה חלקית | 2-3% |
|
||||
| היטל השבחה | 3-4% |
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase (עיבוד מסקנות בלוק י)
|
||||
- Cognitive complexity: low
|
||||
- Accuracy: high-precision (הוראות חייבות להיות חד-משמעיות)
|
||||
- Temperature: 0.1 | Thinking: low | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block יב: חתימות / Signatures
|
||||
|
||||
**ID:** `block-yod-bet`
|
||||
**Akoma Ntoso:** `conclusions > signature`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Authentication — אישור פורמלי של ההחלטה.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: "ניתנה פה אחד" + תאריך עברי/לועזי + טבלת חתימות
|
||||
- Sources: none
|
||||
|
||||
**Constraints:**
|
||||
- MUST: "ניתנה פה אחד", תאריך, יו"ר + מזכיר/ה
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. כללי גזירת פרמטרים
|
||||
|
||||
פרמטרי העיבוד נגזרים ממאפייני התוכן, לא נקבעים שרירותית:
|
||||
|
||||
### Temperature — נגזר מסוג הייצור
|
||||
|
||||
| Generation type | Temperature | נימוק |
|
||||
|----------------|-------------|-------|
|
||||
| template-fill | 0 | אין צורך בשפה — מילוי שדות |
|
||||
| reproduction | 0 | נאמנות מוחלטת למקור. אפס יצירתיות |
|
||||
| paraphrase | 0.1 | מרווח מינימלי לניסוח בשפה של דפנה |
|
||||
| guided-synthesis | 0.2 | גמישות בארגון וחיבור מקורות, לא בתוכן |
|
||||
| analytical-reasoning | 0.3-0.4 | צריך ליצור קשרים בין עקרונות משפטיים |
|
||||
| rhetorical-construction | 0.4-0.5 | טווח ביטוי רחב לכתיבה משכנעת ואלגנטית |
|
||||
|
||||
### Thinking budget — נגזר ממורכבות קוגניטיבית
|
||||
|
||||
| Cognitive task | Budget | נימוק |
|
||||
|---------------|--------|-------|
|
||||
| template-fill / lookup | off | אין צורך בחשיבה |
|
||||
| sequential-extraction | low | חילוץ מידע חד-שלבי |
|
||||
| multi-source-integration | medium | צריך להצליב מקורות |
|
||||
| legal-analysis-with-CREAC | max (16K+) | חשיבה רב-שלבית: מסקנה → כלל → הסבר → יישום |
|
||||
|
||||
### Model — נגזר מדרישת דיוק
|
||||
|
||||
| Accuracy profile | Model | נימוק |
|
||||
|-----------------|-------|-------|
|
||||
| factual-precision | sonnet | מהיר, מדויק לחילוץ עובדות |
|
||||
| precision + interpretation | opus | נדרש לפרשנות תכנית / ציטוט מובנה |
|
||||
| precision + creativity | opus | נדרש לניתוח משפטי מורכב ורטוריקה |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. מתודולוגיית משקלות
|
||||
|
||||
משקל כל בלוק נקבע על ידי שקלול 4 גורמים:
|
||||
|
||||
### 4.1 Communicative Weight (40%)
|
||||
מה חלקו של הבלוק בתכלית ההחלטה? ההחלטה באה לעשות דבר אחד: להכריע במחלוקת ולנמק. בלוק י (דיון) הוא ליבת התכלית. בלוקים א-ד (כותרות) הם עטיפה.
|
||||
|
||||
### 4.2 Reader Attention Distribution (20%)
|
||||
מבוסס על מחקרי F-pattern ו-primacy/recency:
|
||||
- **פתיחה** (בלוקים ה-ו): קשב גבוה (primacy effect)
|
||||
- **אמצע** (בלוקים ז-ח): scanning — הקורא מחפש טענות ספציפיות
|
||||
- **דיון** (בלוק י): deep reading — הקורא מחפש נימוקים
|
||||
- **סיום** (בלוק יא): קשב גבוה (recency effect)
|
||||
|
||||
### 4.3 Judicial Review Requirement (25%)
|
||||
מה שופט בבית משפט לעניינים מנהליים יבדוק ("מבחן השופט"):
|
||||
- **תשתית עובדתית** (בלוק ו): מלאה ומדויקת?
|
||||
- **שמיעת צדדים** (בלוקים ז-ח): נתנו מלוא יום בבית דין?
|
||||
- **סבירות ומידתיות** (בלוק י): ההכרעה מנומקת ומאוזנת?
|
||||
- **התייחסות לטענות** (בלוק י): כל טענה קיבלה מענה?
|
||||
|
||||
### 4.4 Empirical Basis (15%)
|
||||
מבוסס על מדידה מהחלטות שפורסמו:
|
||||
- הכט 1180-1181 (דחייה, 02.2026)
|
||||
- בית הכרם 1126/25 (קבלה חלקית, 03.2026)
|
||||
- אריאלי 1078+1083 (קבלה, 03.2026)
|
||||
|
||||
המשקלות ב-SKILL.md סעיף 3.2 (יחסי הזהב) משמשים כבסיס אמפירי שאומת על ידי שלושת הגורמים האנליטיים.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. כללי ולידציה
|
||||
|
||||
### 5.1 סדר בלוקים
|
||||
- בלוקים חייבים להופיע בסדר א עד יב
|
||||
- בלוקים א-ה ויב נדרשים בכל החלטה
|
||||
- בלוק ט אופציונלי (רק כשיש מורכבות תכנונית)
|
||||
|
||||
### 5.2 Content Constraints
|
||||
- **רקע ניטרלי (בלוק ו):** אם סעיף מכיל ציטוט ישיר מצד או מילת שיפוט → לא שייך כאן
|
||||
- **טענות מקוריות בלבד (בלוק ז):** רק מכתבי ערר/תשובה. השלמות → בלוק ח
|
||||
- **ללא כפילות (בלוק י):** הפניה לבלוקים קודמים, לא חזרה. חריג: "נשוב על כך כי..." (חזרה מכוונת עם שכבה חדשה)
|
||||
- **הליכים ללא הערכה (בלוק ח):** תיעוד מה הוגש, לא הערכה של חוזק הטענות
|
||||
|
||||
### 5.3 Weight Compliance
|
||||
- משקל כל בלוק (ספירת מילים / סה"כ) צריך להיות בטווח המוגדר **±10%**
|
||||
- אם בלוק י < 30% → flag: דיון לא מפותח מספיק
|
||||
- אם בלוק ו > 35% → flag: רקע מנופח, בדוק שאין תוכן טענתי
|
||||
|
||||
### 5.4 Structural Integrity
|
||||
- מספור סעיפים רציף מ-1 עד הסוף, ללא איפוס בין בלוקים
|
||||
- כל הגדרת "להלן" חייבת להופיע לפני השימוש הראשון בה
|
||||
- כל טענה בבלוק ז חייבת לקבל מענה בבלוק י (ישיר או "למעלה מן הצורך")
|
||||
- כותרות פרקים: David 14pt, bold, קו תחתון, מרכז
|
||||
- כותרות משנה: David 12pt, bold, מרכז, ללא קו תחתון
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. גרף תלויות בין בלוקים
|
||||
|
||||
```
|
||||
א (כותרת) → עצמאי
|
||||
ב (הרכב) → עצמאי
|
||||
ג (צדדים) → עצמאי
|
||||
ד (כותרת) → עצמאי
|
||||
ה (פתיחה) → תלוי ב: ג (שמות צדדים להגדרות "להלן")
|
||||
ו (רקע) → תלוי ב: ה (הגדרות). מספור ממשיך מ-ה.
|
||||
ז (טענות) → תלוי ב: ו (מספור). מפנה ל: ה, ו (הגדרות)
|
||||
ח (הליכים) → תלוי ב: ז (מספור). מפנה ל: ז (טענות מקוריות)
|
||||
ט (תכניות) → תלוי ב: ח (מספור). אופציונלי. מפנה ל: ו (הגדרות תכניות)
|
||||
י (דיון) → תלוי ב: **כל** הבלוקים ה-ט. מפנה ל: כולם.
|
||||
יא (סיכום) → תלוי ב: י (מסקנות). מפנה ל: י בלבד.
|
||||
יב (חתימות) → עצמאי
|
||||
```
|
||||
43
docs/case-migration-tracker.md
Normal file
43
docs/case-migration-tracker.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# מעקב העברת תיקים מ-Legacy למערכת החדשה
|
||||
|
||||
נוצר: 2026-04-04
|
||||
|
||||
## תיקים עם החלטה סופית
|
||||
|
||||
| # | מספר תיק | שם | סוג | חומרי מקור | הועבר? | הערות |
|
||||
|---|----------|-----|------|------------|--------|-------|
|
||||
| 1 | 1180-1181 | הכט | רישוי | ערר(2), תשובה(3), פרוטוקול(1) | V | |
|
||||
| 2 | 1126-25 | בית הכרם תמ"א 38 | רישוי | ערר(4), תשובה(6), פרוטוקול(5) | V | |
|
||||
| 3 | 8255-25 | אפרים אבי | בל"מ | ערר(1), תשובה(1), פסיקה(2) | | |
|
||||
| 4 | 8047-24 | משכן אליהו | היטל השבחה | ערר(2), תשובה(2), פרוטוקול(2) | | |
|
||||
| 5 | 8007-24 | עומר דרוויש | שומה מכריעת | ערר(2), תשובה(2), פרוטוקול(2) | | |
|
||||
| 6 | 8141-23 | אזורים | היטל השבחה | ערר(1), תשובה(1), פרוטוקול(1) | | |
|
||||
| 7 | 9005-24 | רמת שלמה | פיצויים | ערר(4), תשובה(4), פרוטוקול(2), חוו"ד(3), פסיקה(2) | | |
|
||||
| 8 | 1113-25 | אייל מבורך | רישוי | פרוטוקול(2) | | |
|
||||
| 9 | 1128-25 | שטרית | רישוי | ערר(1), תשובה(2), פרוטוקול(1), פסיקה(3) | | |
|
||||
| 10 | 1130-25 | קרית יערים-1 | רישוי | ערר(4), תשובה(16), פרוטוקול(28) | | |
|
||||
| 11 | 1194+1199 | קרית יערים-2 | רישוי | ערר(10), תשובה(8), פרוטוקול(7) | | |
|
||||
| 12 | 1130-25 | ליבמן | רישוי | ערר(2), תשובה(6), פרוטוקול(4) | | |
|
||||
|
||||
## תיקים בטיוטה / בתהליך
|
||||
|
||||
| # | מספר תיק | שם | סוג | חומרי מקור | הועבר? | הערות |
|
||||
|---|----------|-----|------|------------|--------|-------|
|
||||
| 1 | 8107-25 | אבו זאהריה | היטל השבחה | ערר(6), תשובה(8), פרוטוקול(4) | | טיוטה + הערות נאוה |
|
||||
| 2 | 8027-25 | תחכמוני 20 | היטל השבחה | ערר(7), תשובה(1), פרוטוקול(2) | | טיוטת DOCX + תכנון מפורט |
|
||||
| 3 | 8070-25 | — | היטל השבחה | ערר(1), תשובה(2), פרוטוקול(2) | | |
|
||||
| 4 | 8136-24 | מרפסות שירות | היטל השבחה | ערר(1), תשובה(1), פרוטוקול(1) | | |
|
||||
| 5 | 1184-25 | — | רישוי | ערר(1), תשובה(2), פרוטוקול(1) | | |
|
||||
| 6 | 1195-25 | — | רישוי | ערר(1), תשובה(2) | | |
|
||||
| 7 | 1200-25 | — | רישוי | ערר(1), תשובה(2) | | |
|
||||
| 8 | 1107-25 | בלוי | רישוי | ערר(2), תשובה(4), פרוטוקול(1), פסיקה(8) | | חומר פסיקתי עשיר |
|
||||
|
||||
## סיכום
|
||||
|
||||
| מדד | כמות |
|
||||
|-----|------|
|
||||
| סה"כ תיקים | 20 |
|
||||
| עם החלטה סופית | 12 |
|
||||
| בטיוטה/תהליך | 8 |
|
||||
| הועברו | 2 |
|
||||
| ממתינים | 18 |
|
||||
240
docs/corpus-analysis.md
Normal file
240
docs/corpus-analysis.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# ניתוח שיטתי של קורפוס ההחלטות — מפת תוכן
|
||||
|
||||
> נוצר: 2026-04-12
|
||||
> מקור: ניתוח 24 החלטות מתוך `/data/training/proofread/`
|
||||
> מטרה: לחלץ דפוסי תוכן בפרק הדיון וההכרעה לפי סוג תיק
|
||||
|
||||
---
|
||||
|
||||
## 1. סקירה כללית של הקורפוס
|
||||
|
||||
### הרכב הקורפוס
|
||||
| סוג | כמות | החלטות |
|
||||
|-----|------|--------|
|
||||
| רישוי ובנייה (1xxx) | 22 | כל ההחלטות מלבד גבאי ובית הכרם |
|
||||
| תמ"א 38 | 1 | בית הכרם 1126+1141 |
|
||||
| סמכות/סף בלבד | 1 | גבאי 1105 |
|
||||
| **היטל השבחה (8xxx)** | **0** | **פער קריטי — אין אף החלטה בקורפוס** |
|
||||
|
||||
### תוצאות
|
||||
| תוצאה | כמות | דוגמאות |
|
||||
|-------|------|---------|
|
||||
| דחייה | 12 | עמית, פרומר, זעיתר, בית שמש, אנשין (חניה), אהרן, שטרית, גבאי, ירושלים שקופה, לבנון, יפה |
|
||||
| קבלה | 5 | טלי-אביב, הראל 1043+1054, הראל 1071+1077, מינץ, לוי |
|
||||
| קבלה חלקית | 4 | אמיתי, בר-און, אנשין (1096), בית הכרם, אואקנין |
|
||||
|
||||
### אורך פרק הדיון
|
||||
- טווח: 465 — 12,000 מילים
|
||||
- ממוצע: ~5,000 מילים
|
||||
- הקצר ביותר: גבאי (465, סמכות בלבד)
|
||||
- הארוך ביותר: תורן/1015 (~11,000, שימוש חורג), מינץ/1071 (~12,000, סבב שני)
|
||||
|
||||
---
|
||||
|
||||
## 2. נושאים שנמצאו בפרקי הדיון — מפת תוכן מלאה
|
||||
|
||||
### 2.1 נושאים תכנוניים (Planning Content)
|
||||
|
||||
| נושא | מופיע ב-X החלטות | עומק טיפוסי | דוגמאות בולטות |
|
||||
|------|-----------------|-------------|----------------|
|
||||
| **ניתוח הוראות תכנית** (ציטוט ישיר, פרשנות) | 18/24 | 3-15 סעיפים | פרומר (MI/200), לבנון (Hal/435), בית הכרם (10038, 16000) |
|
||||
| **חניה** (חישוב, נספח תנועה, חלופות) | 8/24 | 5-15 סעיפים | אנשין-1096 (הרחבה), בית הכרם (הרחבה), אנשין-1109, לוי |
|
||||
| **קווי בניין ומרווחים** | 7/24 | 3-10 סעיפים | אמיתי, אואקנין, שטרית, בר-און |
|
||||
| **גובה/קומות** | 4/24 | 3-6 סעיפים | לבנון (הרחבה), בר-און, לוי |
|
||||
| **סביבה ואופי שכונה** | 6/24 | 2-5 סעיפים | זעיתר (טיפולוגיה), פרומר (חקלאי), בית הכרם |
|
||||
| **שימושים מותרים/שימוש חורג** | 2/24 | 5-20 סעיפים | תורן (הרחבה חריגה), יפה |
|
||||
| **שימור** | 2/24 | 2-5 סעיפים | בית הכרם, בר-און (עצים) |
|
||||
| **טופוגרפיה/טיפולוגיה** | 2/24 | 3-8 סעיפים | זעיתר (הרחבה), לבנון |
|
||||
| **תכנית אב כמסגרת** | 2/24 | 2-3 סעיפים | בית הכרם (16000), תורן (צור הדסה) |
|
||||
| **אינטרס ציבורי (חיזוק/התחדשות)** | 2/24 | 3-8 סעיפים | בית הכרם (תמ"א 38), מינץ |
|
||||
| **היררכיית תכניות** (ארצית→מחוזית→מקומית) | 3/24 | 5-12 סעיפים | פרומר (הרחבה), לבנון, תורן |
|
||||
| **נספח בינוי** (ניתוח פרטני) | 5/24 | 3-8 סעיפים | לבנון, לוי, מינץ, בר-און, בית שמש |
|
||||
| **פגיעה בשכנים** (צל, פרטיות, רעש) | 5/24 | 2-5 סעיפים | אמיתי, שטרית, בית הכרם, אואקנין, זעיתר |
|
||||
| **עצים/נוף** | 3/24 | 1-3 סעיפים | בר-און, בית שמש, בית הכרם |
|
||||
|
||||
### 2.2 נושאים משפטיים (Legal Content)
|
||||
|
||||
| נושא | מופיע ב-X החלטות | עומק טיפוסי |
|
||||
|------|-----------------|-------------|
|
||||
| **סמכות/זכות ערר** (ס' 152, 12ב) | 10/24 | 3-12 סעיפים |
|
||||
| **הלכת שפר** (ערר על היתר תואם תכנית) | 8/24 | 2-5 סעיפים |
|
||||
| **תימוכין קנייניים** (property feasibility) | 6/24 | 5-15 סעיפים |
|
||||
| **סטייה ניכרת** (תקנה 2) | 5/24 | 3-8 סעיפים |
|
||||
| **שיהוי** (delay/laches) | 5/24 | 2-5 סעיפים |
|
||||
| **מידתיות** (proportionality) | 4/24 | 2-5 סעיפים |
|
||||
| **עבריינות בנייה** (building violations) | 6/24 | 2-5 סעיפים |
|
||||
| **שיקול דעת הוועדה המקומית** | 8/24 | 2-5 סעיפים |
|
||||
| **קניין vs. תכנון** (הפרדת סמכויות) | 7/24 | 3-10 סעיפים |
|
||||
| **הכשרת בנייה קיימת** (regularization) | 3/24 | 5-12 סעיפים |
|
||||
|
||||
---
|
||||
|
||||
## 3. דפוסי "דיון תכנוני" שזוהו
|
||||
|
||||
### 3.1 מתי דפנה מקיימת דיון תכנוני מקיף?
|
||||
|
||||
**תמיד** כאשר:
|
||||
- הערר עוסק בהתאמה לתכנית (ייעוד, שימוש, גובה, בנייה)
|
||||
- יש שאלה של סטייה מהוראות תכנית
|
||||
- הנושא הוא חניה/תשתיות
|
||||
- התיק מערב תמ"א 38 או התחדשות עירונית
|
||||
|
||||
**לעולם לא** כאשר:
|
||||
- התיק הוא סף/סמכות בלבד (גבאי)
|
||||
- השאלה היא קניינית טהורה (טלי-אביב/1043+1054, בית שמש/1180+1181)
|
||||
|
||||
**עומק משתנה** כאשר:
|
||||
- יש מספר נושאים שחלקם תכנוניים — הדיון התכנוני מוגבל לנושאים הרלוונטיים
|
||||
|
||||
### 3.2 איך דפנה בונה דיון תכנוני — הדפוס
|
||||
|
||||
**שלב 1: הקשר תכנוני רחב (2-8 סעיפים)**
|
||||
- תכניות חלות ברמה הרלוונטית (מקומית, מחוזית, ארצית)
|
||||
- ייעוד הקרקע, שימושים מותרים
|
||||
- אופי הסביבה, מרקם בנוי
|
||||
- *דוגמה*: פרומר — 12 סעיפים על MI/200, TAMA 35, TAMAM 30/1
|
||||
|
||||
**שלב 2: ציטוט ישיר מהוראות תכנית (3-15 סעיפים)**
|
||||
- בלוקים ארוכים (200-600 מילים) של הוראות תכנית
|
||||
- הדגשות בולד על המילים הרלוונטיות
|
||||
- "הדגשת הח"מ" / "הדגשת הח.מ."
|
||||
- *דוגמה*: בית הכרם — 400+ מילים מהוראות חניה של תכנית 5166ב
|
||||
|
||||
**שלב 3: יישום על המקרה הספציפי (3-8 סעיפים)**
|
||||
- הוראה → עובדה → מסקנה
|
||||
- "הנה מה שאומרת התכנית, הנה מה שקורה בפועל, הנה המסקנה"
|
||||
- *דוגמה*: לבנון — השוואת חתכים של נספח בינוי עם הבקשה
|
||||
|
||||
**שלב 4: מסקנה תכנונית (1-3 סעיפים)**
|
||||
- האם הבקשה תואמת/סוטה
|
||||
- האם הסטייה מוצדקת
|
||||
- מה צריך לתקן
|
||||
|
||||
### 3.3 הדפוס של "דיון תכנוני" לפי סוג נושא
|
||||
|
||||
| נושא | סדר ניתוח טיפוסי | רמת עומק |
|
||||
|------|-----------------|----------|
|
||||
| **חניה** | הוראות תכנית → תקנות חניה → נספח תנועה → חישוב → חלופות (קרן חניה, חפיפה, תחבורה ציבורית) | עמוק מאוד (8-15 סעיפים) |
|
||||
| **קווי בניין** | הוראת תכנית → סטייה ניכרת? (תקנה 2(19)) → מידתיות → פגיעה בשכנים | בינוני-עמוק (5-10 סעיפים) |
|
||||
| **גובה** | הוראת תכנית → נספח בינוי → מטרת ההגבלה → סטייה ניכרת? | בינוני (4-8 סעיפים) |
|
||||
| **ייעוד/שימוש** | פרשנות תכנית → היררכיית תכניות → פרשנות מהותית → יישום | עמוק מאוד (10-20 סעיפים) |
|
||||
| **שכנות** | עובדות (סיור) → השפעה (צל, פרטיות, רעש) → מידתיות | בינוני (3-6 סעיפים) |
|
||||
| **סביבה** | תכנית אב → אופי שכונה → מרקם → השתלבות | בינוני (3-5 סעיפים) |
|
||||
|
||||
---
|
||||
|
||||
## 4. דפוסים חוצי-החלטות
|
||||
|
||||
### 4.1 מבנה הדיון — סדר הנושאים
|
||||
1. **שאלות סף** (אם יש) — סמכות, זכות ערר, שיהוי
|
||||
2. **הקשר תכנוני רחב** — תכניות, ייעוד, סביבה
|
||||
3. **ניתוח ענייני** — נושא אחר נושא, כל אחד ב-CREAC
|
||||
4. **מענה לטענות ספציפיות** — עובר על כל טענה מבלוק ז
|
||||
5. **מסקנה** — תוצאה + הוראות אופרטיביות
|
||||
|
||||
### 4.2 כמה תכנון יש בכל החלטה?
|
||||
|
||||
| דרגה | תיאור | החלטות |
|
||||
|------|-------|--------|
|
||||
| **כבד** (>50% תכנון) | הדיון הוא בעיקר תכנוני | פרומר, זעיתר, בית הכרם, תורן, לבנון |
|
||||
| **מאוזן** (30-50%) | שילוב תכנון + משפט | עמית, אמיתי, בר-און, אנשין-1096, אואקנין, לוי, שטרית |
|
||||
| **קל** (<30%) | בעיקר משפטי, תכנון מינימלי | בית שמש, אנשין-1109, יפה |
|
||||
| **אין** (0%) | רק משפטי/סמכות | טלי-אביב, הראל 1043+1054, גבאי, ירושלים שקופה |
|
||||
|
||||
### 4.3 פסיקה חוזרת (Recurring Case Law)
|
||||
| פסיקה | נושא | מופיעה ב-X החלטות |
|
||||
|-------|------|-------------------|
|
||||
| הלכת שפר (עע"מ 317/10) | ערר על היתר תואם תכנית | 8 |
|
||||
| הלכת עייזן (בג"ץ 1578/90) | תימוכין קנייניים | 6 |
|
||||
| הלכת בן-יקר-גת | סטייה ניכרת | 4 |
|
||||
| ערר אדלר 1181/22 | שיקול דעת תמ"א 38 | 2 |
|
||||
| עע"מ 3975/22 קרן נכסים | קניין vs. תכנון | 4 |
|
||||
|
||||
### 4.4 טכניקות ניתוח ייחודיות
|
||||
1. **פרשנות הרמונית** — כשיש מספר תכניות, דפנה מפרשת אותן ביחד (תורן)
|
||||
2. **בדיקת תקדימים עובדתית** — הוועדה בדקה בעצמה 3 נכסים שנטענו כתקדים (לבנון)
|
||||
3. **ציטוט מהחלטה מרכזת** — במקום לצטט 7 פס"ד, מצטטת אחד שריכז את כולם
|
||||
4. **מבחן "המגרש הריק"** — להכשרת בנייה קיימת (אמיתי)
|
||||
5. **מיפוי מתחים** — רשימת 3-6 מתחים לפני הניתוח (בית הכרם)
|
||||
6. **"למעלה מן הצורך"** — דיון obiter אחרי הכרעה בסף (עמית, בית שמש)
|
||||
|
||||
---
|
||||
|
||||
## 5. פערים שזוהו
|
||||
|
||||
### 5.1 פער קריטי: אין החלטות היטל השבחה בקורפוס
|
||||
למרות שהמערכת מגדירה 3 סוגי עררים (רישוי, היטל השבחה, פיצויים) — **כל 24 ההחלטות הן רישוי ובנייה**. אין לנו אף מודל לכתיבת החלטה בהיטל השבחה.
|
||||
|
||||
### 5.2 פער: לא כל נושא תכנוני מכוסה
|
||||
נושאים שמופיעים רק בהחלטה אחת-שתיים:
|
||||
- שימור → רק בית הכרם
|
||||
- תמ"א 38 → רק בית הכרם
|
||||
- שימוש חורג → רק תורן
|
||||
- טיפולוגיה/טופוגרפיה → רק זעיתר
|
||||
- תכנית אב כמסגרת → רק בית הכרם + תורן
|
||||
|
||||
### 5.3 פער: הפרומפט הנוכחי לא מכיל "צ'קליסט תוכן"
|
||||
הפרומפט של block-yod (שורות 198-234 ב-block_writer.py) אומר:
|
||||
- ✅ CREAC methodology
|
||||
- ✅ ענה על כל טענה
|
||||
- ✅ צטט פסיקה
|
||||
- ❌ **אין**: "בתיק רישוי, כסה את הנושאים התכנוניים הרלוונטיים"
|
||||
- ❌ **אין**: צ'קליסט תוכן לפי סוג ערר
|
||||
- ❌ **אין**: "הקשר תכנוני רחב" כמרכיב חובה
|
||||
|
||||
### 5.4 פער: הבחנה לא מספיקה בין תת-סוגי רישוי
|
||||
תיקי רישוי שונים מאוד זה מזה:
|
||||
- **סמכות/סף** — דיון משפטי טהור, אין צורך בתכנון
|
||||
- **קנייני** — תימוכין קנייניים, אין צורך בתכנון
|
||||
- **תכנוני מובהק** — ייעוד, חניה, גובה — דיון תכנוני מקיף
|
||||
- **שימוש חורג** — פרשנות תכניות, דיון תכנוני עמוק
|
||||
- **הקלה** — מידתיות + תכנון
|
||||
- **תמ"א 38** — איזון אינטרסים + תכנון
|
||||
|
||||
---
|
||||
|
||||
## 6. המלצות לשלב הבא
|
||||
|
||||
### 6.1 צ'קליסט תוכן מוצע לערר רישוי ובנייה
|
||||
```
|
||||
בהתאם לנושא הערר, הדיון צריך לכלול:
|
||||
|
||||
□ הקשר תכנוני רחב (תמיד כשהערר מגיע למריט):
|
||||
- תכניות חלות (מקומית, מחוזית, ארצית — לפי הצורך)
|
||||
- ייעוד הקרקע
|
||||
- אופי הסביבה
|
||||
|
||||
□ ניתוח הוראות תכנית (כשיש שאלה של התאמה/סטייה):
|
||||
- ציטוט ישיר מהוראות רלוונטיות
|
||||
- פרשנות — תכלית ההוראה
|
||||
- יישום על המקרה
|
||||
|
||||
□ חניה (כשרלוונטי):
|
||||
- הוראות תכנית + נספח תנועה
|
||||
- חישוב מקומות נדרשים vs. מסופקים
|
||||
- חלופות (קרן חניה, חפיפה, תח"צ)
|
||||
|
||||
□ שכנות/פגיעה (כשרלוונטי):
|
||||
- ממצאי סיור
|
||||
- צל, פרטיות, רעש, נוף
|
||||
- מידתיות
|
||||
|
||||
□ קווי בניין (כשרלוונטי):
|
||||
- הוראת תכנית
|
||||
- סטייה ניכרת — תקנה 2(19)
|
||||
- הצדקה/מידתיות
|
||||
|
||||
□ גובה/קומות (כשרלוונטי):
|
||||
- הוראת תכנית + נספח בינוי
|
||||
- מטרת ההגבלה
|
||||
- סטייה ניכרת — תקנה 2(10)
|
||||
```
|
||||
|
||||
### 6.2 הבחנה בין תת-סוגים
|
||||
הפרומפט צריך לזהות את סוג הערר ולהתאים את הצ'קליסט:
|
||||
- **ערר סמכות/סף** → ללא דיון תכנוני
|
||||
- **ערר קנייני** → דיון משפטי, ללא תכנון
|
||||
- **ערר מהותי** → דיון תכנוני מקיף + משפטי
|
||||
|
||||
### 6.3 צורך דחוף: החלטות היטל השבחה
|
||||
צריך להוסיף לקורפוס לפחות 5-10 החלטות של היטל השבחה לפני שהמערכת יכולה לכתוב החלטות בתחום הזה.
|
||||
148
docs/decision-block-mapping.md
Normal file
148
docs/decision-block-mapping.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# מיפוי מדויק של 3 החלטות לבלוקים
|
||||
|
||||
**תאריך:** 2 באפריל 2026
|
||||
**שיטה:** קריאה מלאה מילה-במילה עם אימות ספירת מילים
|
||||
|
||||
---
|
||||
|
||||
## 1. הכט 1180-1181 (דפנה תמיר, דחייה, רישוי)
|
||||
|
||||
**מקור:** data/training/היתר בניה-בית שמש-1180+1181-החלטה.docx
|
||||
**שורות:** 105 | **מילים:** 4,433
|
||||
|
||||
| שורות | בלוק | תוכן | הערות |
|
||||
|-------|------|------|-------|
|
||||
| שורה 1 | ה — פתיחה | "לפנינו שני עררים..." הגדרות להלן | פתיחה קלאסית |
|
||||
| שורות 2-11 | ו — רקע | הבניין, התכנית, הבקשה, חתימות, התנגדויות, תקנה 37 | רקע מינימלי ~470 מילים |
|
||||
| שורה 12 | ז — כותרת | "תמצית טענות הצדדים" | — |
|
||||
| שורות 13-24 | ז — טענות עוררים | 11 טענות: המצאה, לובי, זכויות, מדרגות, חניות, עץ, רוב, סדובסקי, חזרה מחתימה, הידברות | — |
|
||||
| שורות 25-34 | ז — עמדת ועדה + משיבים | "עמדת המשיבים" → "הוועדה המקומית" (8 טענות): תואמת תכנית, רוב, היבטים תכנוניים, המצאה, חזרה, תכנית אושרה, חניות, עץ | — |
|
||||
| שורות 35-46 | ז — מבקשי היתר | 11 טענות: המצאה, חתימות, לובי, רוב, תכנית, זכויות, חניות, עץ, הידברות | — |
|
||||
| שורה 47 | י — כותרת | "דיון והכרעה" | — |
|
||||
| שורות 48-96 | י — דיון | פתיחה עם מסקנה → ס' 152 → פסיקה (נגאח, הימנותא, דסטגר, קרן-נכסים) → סטייה מתכנית → מדרגות, דלת, עץ, חניות → סמכות ועדה מקומית → פנייה לעוררת | **אין בלוק ט נפרד** — ניתוח ס' 152 והפסיקה משולב בדיון |
|
||||
| שורה 97 | יא — כותרת | "סיכום" | — |
|
||||
| שורות 98-104 | יא — סיכום | 6 תתי-סעיפים אופרטיביים: אין זכות ערר, תואמת תכנית, אין סטיה, לא נפל פגם, טענות קנייניות, עץ | — |
|
||||
| שורה 105 | יב — חתימות | "ניתנה פה אחד היום, כ"ב שבט תשפ"ו, 09 פברואר 2026" | — |
|
||||
|
||||
**בלוקים קיימים:** ה, ו, ז, י, יא, יב
|
||||
**בלוקים חסרים:** ח (הליכים), ט (תכניות — משולב בדיון)
|
||||
|
||||
---
|
||||
|
||||
## 2. בית הכרם 1126/25+1141/25 (דפנה תמיר, קבלה חלקית, רישוי)
|
||||
|
||||
**מקור:** data/training/תמא 38-בית הכרם-1126+1141-החלטה.docx (גרסה סופית מנבו)
|
||||
**שורות:** 183 | **מילים:** 6,249
|
||||
|
||||
| שורות | בלוק | תוכן | הערות |
|
||||
|-------|------|------|-------|
|
||||
| שורות 1-7 | — מטא-דאטה נבו | ספרות, חקיקה שאוזכרה | **לא חלק מההחלטה** |
|
||||
| שורה 8 | ה — פתיחה | "לפנינו שני עררים..." + הגדרות | — |
|
||||
| שורה 9 | ו — כותרת | "רקע" | — |
|
||||
| שורות 10-56 | ו — רקע | מקרקעין, תכניות (911, 10038, 16000), בקשה להיתר, החלטת ועדת משנה + התנגדויות (מצוטטות), תיקונים, שטחים, מרפסות, פיתוח, נגישות | רקע מפורט מאוד ~1,100 מילים. **כולל ציטוט מפרוטוקול ועדת המשנה** |
|
||||
| שורות 57-59 | ח — הליכים | דיון 9.12.2025, החלטת ביניים, השלמת טיעון 3.2.2026 | **בלוק ח קיים!** אבל קצר (3 שורות). מופיע **לפני** הטענות |
|
||||
| שורה 60 | ז — כותרת | "תמצית טענות הצדדים" | — |
|
||||
| שורות 61-76 | ז — טענות עוררים | מרכז קהילתי (ראייה אזורית, חניה, עיר גנים), תושבים (פגמי פרסום, חריגת שטחים, ס' 6.5 תמ"א 38, שימור, עצים, בור מים, חניה, פרטיות) | — |
|
||||
| שורות 77-84 | ז — עמדת ועדה מקומית | תואמת 10038, חניה (מגרש כלוא, כופר חניה), שטחים, שימור | — |
|
||||
| שורות 85-91 | ז — מבקשי היתר | היקף 126%, תואמת, צמצום 42%, חניה, שימור, ראייה אזורית | — |
|
||||
| שורה 92 | י — כותרת | "דיון והכרעה" | — |
|
||||
| שורות 93-102 | י — **מיפוי מתחים** | 6 מתחים: בית בודד, מדיניות, שימור, קווי בניין, מגרש כלוא, חריג לסביבה | **פתיחה ייחודית לקבלה חלקית** |
|
||||
| שורות 103-113 | י — ניתוח תכניות | 10038, 16000 (תכנית אב), 911, שימור, ס' 4.1.2.2(5), ס' 6.5.9 | **אין בלוק ט נפרד** — משולב בדיון |
|
||||
| שורות 114-149 | י — ניתוח נושאי | חניה (5166ב, כופר, מגרש כלוא, רכבת קלה), קווי בניין (שימור vs מרחק), מטרדי בנייה (ערר 1192/18, ערר 1156/18), עצים, בור מים | — |
|
||||
| שורות 150-177 | י — ציטוט ערר מובשוביץ + התחדשות עירונית | ציטוט נרחב (~400 מילים) מערר מובשוביץ, ספרות (גדרון ונמדר), פסיקה (לזובסקי, ערר 76/14) | — |
|
||||
| שורה 178 | יא — כותרת | "סיכום" | — |
|
||||
| שורות 179-181 | יא — סיכום | "מתקבל באופן חלקי" + 2 הוראות: בחינת מרווח, תכנית ארגון אתר | **סיכום מינימלי** — 88 מילים |
|
||||
| שורות 182-183 | — מטא-דאטה נבו | "נוסח מסמך זה כפוף..." | **לא חלק מההחלטה** |
|
||||
|
||||
**בלוקים קיימים:** ה, ו, ח (קצר, לפני טענות), ז, י, יא
|
||||
**בלוקים חסרים:** ט (משולב בדיון)
|
||||
|
||||
**ממצאים ייחודיים:**
|
||||
- ח מופיע **לפני** ז (שונה מהכט ואריאלי)
|
||||
- פתיחת דיון = "מיפוי מתחים" — 6 מתחים בתבליטים
|
||||
- סיכום מינימלי — הוראות אופרטיביות בלבד
|
||||
- ציטוט ארוך מערר מובשוביץ (~400 מילים)
|
||||
|
||||
---
|
||||
|
||||
## 3. אריאלי 1078+1083/24 (שרית אריאלי, קבלה, רישוי)
|
||||
|
||||
**מקור:** data/training/ (legacy — גרסה מנבו, לא בקורפוס הנוכחי)
|
||||
**שורות:** 171 | **מילים:** 10,748
|
||||
|
||||
| שורות | בלוק | תוכן | הערות |
|
||||
|-------|------|------|-------|
|
||||
| שורות 1-13 | א-ד — כותרת | הרכב, צדדים, חקיקה, "החלטה" | — |
|
||||
| שורות 15-16 | ה — פתיחה | "עניינה של החלטה זו..." + הגדרות | **שונה מדפנה** — לא "לפנינו" |
|
||||
| שורה 17 | ו — כותרת | **"פתח דבר"** | **כותרת ייחודית לאריאלי** |
|
||||
| שורות 18-32 | ו — רקע | מקרקעין (מוסררה), היסטוריה תכנונית (2015, 2017, 2020, 2023), שימור (אתר 3890), סביבה (GIS), עוררים | רקע מפורט ~1,500 מילים |
|
||||
| שורה 33 | ז — כותרת | "טענות הצדדים" | **לא** "תמצית טענות הצדדים" |
|
||||
| שורות 34-39 | ז — עוררים 1083 | שימור, מסה, תמ"א 38, ועדת שימור, מרחב ציבורי | — |
|
||||
| שורות 40-42 | ז — עורר 1078 | נוף להר הבית, גובה | — |
|
||||
| שורות 43-49 | ז — ועדה מקומית | תואמת, שימור, חניה, מקלט | — |
|
||||
| שורות 50-55 | ז — **עמדת ועדת שימור** | לא עקבית, שינוי עמדות | **צד נוסף** שלא קיים בדפנה |
|
||||
| שורות 56-62 | ז — מבקש היתר | מגרש ייחודי, מרחקים, מקלט, סטודנטים | — |
|
||||
| שורה 63 | ח — כותרת | **"ההליכים בפני וועדת הערר"** | — |
|
||||
| שורות 64-66 | ח — דיון | דיון 10.11.2024, טענות, מבקש היתר | — |
|
||||
| שורות 67-69 | ח — סיור | סיור 18.3.2025, אדריכלית דינור, תצפית | — |
|
||||
| שורות 70-75 | ח — החלטת ביניים + עמדת שימור | חתכי בינוי, חלופה, עמדת שימור 31.12.2025 | — |
|
||||
| שורות 76-86 | ח — השלמות טיעון | מבקש היתר 1.1.2026, השוואת בניינים ברחוב הע"ח, חלופה להנמכה, תגובת עוררים 12.1.26, עמדת שימור 1.2.26 | — |
|
||||
| שורות 87-92 | ח — תגובות נוספות | תגובת עוררים להשלמה, עמדת שימור סופית | **ח = 30 שורות, ~2,900 מילים** |
|
||||
| שורה 93 | ט — כותרת | **"התכניות החלות על המקרקעין"** | — |
|
||||
| שורות 94-97 | ט — תכניות סטטוטוריות | 3188 (1985), 3188א (1993), 3188ב (1995) | — |
|
||||
| שורות 98-104 | ט — ערר מעלומי | ציטוט נרחב מערר מעלומי על אופי שימורי | — |
|
||||
| שורות 105-113 | ט — תמ"א 38 + 10038 + רבדים | סעיף 19, שלושה רבדים של שימור, שיקול דעת | **ט = 21 שורות, ~1,800 מילים** |
|
||||
| שורה 114 | י — כותרת | **"דיון והכרעה:"** | נקודתיים בסוף — ייחודי |
|
||||
| שורות 115-117 | י — מסקנה בפתיחה | "שוכנענו כי הבקשה מהווה בינוי מאסיבי..." | CREAC — מסקנה קודם |
|
||||
| שורות 118-149 | י — ניתוח | אינטרס חיזוק, סבירות, חזית חמישית, דירוג, שימור חזיתות, ערר אדלר, מובשוביץ | — |
|
||||
| שורות 150-157 | י — תקדים שלילי + דרך המלך | תכנית נקודתית, התנהלות גורמי מקצוע | — |
|
||||
| שורות 158-164 | י — נושאים נוספים | נוף (עורר 1078), מקלט, זכות עמידה, פרסום | — |
|
||||
| שורה 165 | יא — כותרת | **"סוף דבר"** | **לא** "סיכום" — שונה מדפנה |
|
||||
| שורות 166-168 | יא — סיכום | "העררים מתקבלים" + הוראות: בקשה מתוקנת, גובה, קווי בניין, יח"ד | — |
|
||||
| שורה 169 | יב — חתימות | "ניתנה פה אחד, ט"ו ניסן תשפ"ו, 02 אפריל 2026" | — |
|
||||
| שורות 170-171 | — מטא-דאטה נבו | "נוסח מסמך זה כפוף..." | **לא חלק מההחלטה** |
|
||||
|
||||
**בלוקים קיימים:** ה, ו, ז, ח (מורחב — 30 שורות), ט (נפרד), י, יא, יב
|
||||
**כל הבלוקים המהותיים קיימים — זו ההחלטה השלמה ביותר**
|
||||
|
||||
**ממצאים ייחודיים:**
|
||||
- פתיחה "עניינה של החלטה זו" (לא "לפנינו")
|
||||
- כותרת רקע "פתח דבר" (לא "רקע")
|
||||
- כותרת טענות "טענות הצדדים" (לא "תמצית טענות הצדדים")
|
||||
- כותרת סיכום "סוף דבר" (לא "סיכום")
|
||||
- בלוק ח מורחב (30 שורות) — דיון + סיור + השלמות + תגובות
|
||||
- בלוק ט נפרד — תכניות + רבדי שימור + תמ"א 38
|
||||
- צד נוסף בטענות: "עמדת ועדת שימור"
|
||||
- CREAC מפורש בדיון — מסקנה בפתיחה ("שוכנענו כי...")
|
||||
|
||||
---
|
||||
|
||||
## סיכום השוואתי
|
||||
|
||||
### סדר בלוקים
|
||||
|
||||
| בלוק | הכט (דחייה) | בית הכרם (חלקית) | אריאלי (קבלה) |
|
||||
|------|------------|-----------------|---------------|
|
||||
| ה — פתיחה | "לפנינו" | "לפנינו" | "עניינה של" |
|
||||
| ו — רקע | "רקע" (אין כותרת) | "רקע" | "פתח דבר" |
|
||||
| ח — הליכים | **לא קיים** | **לפני ז** (3 שורות) | **אחרי ז** (30 שורות) |
|
||||
| ז — טענות | "תמצית טענות הצדדים" | "תמצית טענות הצדדים" | "טענות הצדדים" |
|
||||
| ט — תכניות | **משולב בדיון** | **משולב בדיון** | **נפרד** |
|
||||
| י — דיון | מסקנה → ס' 152 → פסיקה → יישום | מיפוי מתחים → ניתוח נושאי | CREAC מפורש |
|
||||
| יא — סיכום | "סיכום" (6 סעיפים) | "סיכום" (2 הוראות) | "סוף דבר" (הוראות) |
|
||||
|
||||
### מה קובע אילו בלוקים קיימים
|
||||
|
||||
| בלוק | כלל | פירוט |
|
||||
|------|-----|-------|
|
||||
| ח — הליכים | **מותנה** — רק כשהיו הליכים מעבר לדיון פשוט | סיור = סמן חזק לבלוק ח מפורט. השלמות טיעון רבות / החלטות ביניים = בלוק ח קצר. דיון פשוט (הצדדים טענו ונגמר) = אין בלוק ח |
|
||||
| ט — תכניות | **תמיד קיים** — רישום התכניות החלות הוא חובה | רישום התכניות: תמיד, כחלק מהרקע (ו) או כפרק נפרד. ניתוח התכניות: בדיון (י), רק כשרלוונטי. פרק ט נפרד רק כשהמורכבות התכנונית מצדיקה ניתוח מקדים (כמו באריאלי — 3 תכניות שימור + 3 רבדים) |
|
||||
|
||||
### מה צריך לתקן ב-parser
|
||||
|
||||
1. פתיחה: לזהות גם "עניינה של" ולא רק "לפנינו"
|
||||
2. רקע: לזהות גם "פתח דבר" כנוסף ל-"רקע"
|
||||
3. טענות: לזהות גם "טענות הצדדים" בלי "תמצית"
|
||||
4. סיכום: לזהות גם "סוף דבר"
|
||||
5. מטא-דאטה נבו: לסנן שורות 1-7 ו-182-183 של בית הכרם, 170-171 של אריאלי
|
||||
6. ח לפני ז: בבית הכרם ח מופיע לפני ז — ה-parser צריך לתמוך בזה
|
||||
204
docs/legal-decision-lessons.md
Normal file
204
docs/legal-decision-lessons.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Legal Decision Writing - Lessons Learned
|
||||
|
||||
Lessons extracted by comparing our planning/drafts against Dafna's published final versions.
|
||||
|
||||
## Source
|
||||
- Published decision: `01_Projects/כתיבת החלטות משפטיות/ערר 1180-1181 הכט/החלטה/הכט 1180-1181.pdf`
|
||||
- Our draft: `01_Projects/כתיבת החלטות משפטיות/ערר 1180-1181 הכט/החלטה/דיון-והכרעה-טיוטה.md`
|
||||
- Date: February 2026
|
||||
|
||||
## What Our Draft Got Right
|
||||
- Section numbering continuity (no resets)
|
||||
- "להלן" definitions with bold formatting
|
||||
- Overall arguments structure (appellants > local committee > permit applicants)
|
||||
- Citation of relevant case law (שפר, הימנותא, דסטגר)
|
||||
- Clear separation between parties' arguments
|
||||
|
||||
## What the Published Version Changed
|
||||
|
||||
### 1. Discussion Section Structure
|
||||
- **Draft:** 6 sub-headers (H2) breaking the discussion into topics
|
||||
- **Published:** ZERO sub-headers. One continuous flow of numbered paragraphs
|
||||
- **Lesson:** The discussion reads as a legal essay, not a structured outline
|
||||
|
||||
### 2. Citation Technique
|
||||
- **Draft:** Each case cited in its own paragraph (7 separate paragraphs for the proprietary claims section)
|
||||
- **Published:** One massive paragraph (~600 words) citing through ערר נגאח 1011-03-25, which itself consolidated all the case law
|
||||
- **Lesson:** "Citation through consolidating decision" technique
|
||||
|
||||
### 3. Paragraph Length in Discussion
|
||||
- **Draft:** Uniform 50-70 words per paragraph
|
||||
- **Published:** Ranges from 20 to 600+ words. Key citation paragraphs are very long.
|
||||
- **Lesson:** Don't fragment long legal arguments into tiny chunks
|
||||
|
||||
### 4. Opening Formula
|
||||
- **Draft:** "לאחר שבחנו את טענות הצדדים... החלטנו שיש לדחות את הערר על הסף"
|
||||
- **Published:** "לאחר שבחנו... החלטנו בשלב ראשון כי... **אך יחד עם זאת ועל מנת לא לצאת בחסר**... מצאנו להוסיף מספר הערות"
|
||||
- **Lesson:** The opening promises both conclusion AND elaboration
|
||||
|
||||
### 5. Summary Section
|
||||
- **Draft:** "סיכום והכרעה" with 5 items (א-ה)
|
||||
- **Published:** "סיכום" with 6 items (א-ו), more specific language
|
||||
- **Lesson:** Title is "סיכום", not "סיכום והכרעה" or "סוף דבר"
|
||||
|
||||
### 6. Transition Phrases (new ones discovered)
|
||||
- "ועל מנת לא לצאת בחסר" - for obiter dicta
|
||||
- "נציין כי טענות אלו נטענו בלשון רפה" - acknowledging weak claims
|
||||
- "עינינו הרואות" - summary after long quote
|
||||
- "נוסיף." - ultra-short transition (one word!)
|
||||
- "אם כך, לעת הזו" - drawing conclusion from citations
|
||||
- "למיטב הבנתנו" - cautious position on pending matter
|
||||
- "נשלים ונציין" - last point before summary
|
||||
|
||||
### 7. New Case Law References (not in our draft)
|
||||
- ערר (מרכז) 1011-03-25 נגאח עבד אל קאדר (consolidating decision on proprietary claims)
|
||||
- עע"מ 3975/22 ב. קרן-נכסים (Supreme Court on proprietary feasibility - extensive quote)
|
||||
- ערר 1071/25 מינץ (own previous decision)
|
||||
- סעיף 71ב(א)(1) לחוק המקרקעין (majority required for common property changes)
|
||||
|
||||
### 8. Substantive Changes
|
||||
- Added response from local committee on parking (columns) and tree (sections 29-30)
|
||||
- Added "על החלטת רשות רישוי מיום 30.11.25" in opening (specific decision date)
|
||||
- Changed expenses from "no order" to "appellants shall bear expenses"
|
||||
- Added "ניתנה פה אחד" (unanimous decision)
|
||||
|
||||
## Applied To
|
||||
- Updated `.claude/skills/legal-decision/SKILL.md` - added Section 7 (Discussion methodology), updated Section 4 (transitions), updated Section 1.3 (structure), updated Section 2.1 (paragraph lengths), updated Section 6 (checklist), updated case law references
|
||||
|
||||
---
|
||||
|
||||
## Lessons from בית הכרם 1126/25 + 1141/25
|
||||
|
||||
### Source
|
||||
- Final version (Draft 9): `04_Archive/ערר-1126-25-תמא-38-בית-הכרם/החלטה/בית הכרם-טיוטת החלטה-9.pdf`
|
||||
- Our planning: `04_Archive/ערר-1126-25-תמא-38-בית-הכרם/סטטוס-תכנון.md`
|
||||
- Date: March 2026
|
||||
- Result: Partial acceptance (קבלה חלקית)
|
||||
|
||||
### What Our Planning Got Right
|
||||
- Overall result prediction (partial acceptance, same operational directives)
|
||||
- Identification of key issues (parking, setback lines, preservation)
|
||||
- Basic structure (background → arguments → discussion → summary)
|
||||
- Content of parties' arguments section
|
||||
- Citation through consolidating decision technique (ערר אדלר)
|
||||
|
||||
### What the Final Version Changed — Critical Gaps
|
||||
|
||||
#### 1. Threshold Question Skipped Entirely
|
||||
- **Planning:** "שכבה 1 — קריטית: אין זכות ערר לפי ס' 152"
|
||||
- **Final:** Zero discussion of right to appeal. Straight to substantive analysis.
|
||||
- **Lesson:** The threshold question (6.1 in skill) is a STRATEGIC TOOL, not mandatory. When the case has strong substantive questions (parking, setback, preservation), Dafna prefers engaging with substance over procedural blocking. This also strengthens the decision against judicial review.
|
||||
|
||||
#### 2. Concentric Circles Model Not Used
|
||||
- **Planning:** 5 defined layers (threshold → merit → parking → setback → specific claims)
|
||||
- **Final:** Different structure — context → tension mapping → issue-by-issue analysis → operational conclusions
|
||||
- **Lesson:** Concentric circles fit REJECTED appeals (like הכט). For partial acceptance, Dafna uses flexible issue-by-issue analysis. The skill's 6.3 is one tool among several, not THE framework.
|
||||
|
||||
#### 3. New Opening Type: "Tension Mapping"
|
||||
- **Final ס' 39:** Lists 6 specific tensions in bullet points before analysis begins
|
||||
- **Pattern:** "בערר דנן עולות שאלות כיצד והאם..." → bulleted list of tensions → "כל הנקודות לעיל עומדות לפנינו ולשם כך..."
|
||||
- **When:** Partial acceptance or cases with multiple complex intersecting issues
|
||||
- **Not in skill:** This is a new opening type distinct from "broad context" (rejected) or "direct conclusion" (accepted)
|
||||
|
||||
#### 4. "Single Building" Weakens TAMA 38 Interest
|
||||
- **Final ס' 41:** "עסקינן בחיזוק בית בודד ועל כן... לא קיים באופן מלא אינטרס חיזוק כזה המצדיק את אישור מלוא הזכויות"
|
||||
- **Not in skill or planning.** New analytical pattern: when TAMA 38 applies to a single house (vs. large apartment building), earthquake protection interest is weaker → more cautious approval of rights, especially setback lines and parking.
|
||||
|
||||
#### 5. Master Plan as "Shield" Against Ad-Hoc Planning Concern
|
||||
- **Final ס' 42:** "קיימת תכנית אב אשר מקלה על בחינת הבקשה... החשש לאישור היתר מכח תכנית 10038 על מגרש בודד ללא ראיה כללית אינו קיים"
|
||||
- **Pattern:** When a master plan exists → cite it to validate individual permit → conclusion: permit "integrates with existing comprehensive vision" rather than creating ad-hoc precedent.
|
||||
|
||||
#### 6. Depth of Plan Provision Citations
|
||||
- **Planning:** Expected general parking analysis
|
||||
- **Final:** Extensive direct quotes from plan provisions (6.8(4), 6.8(9), traffic appendix, 5166b) — 400+ words of plan citations with interleaved analysis
|
||||
- **Lesson:** For parking/infrastructure issues, Dafna goes very deep into plan provisions with direct quotes, not summaries.
|
||||
|
||||
#### 7. Ultra-Minimal Summary for Partial Acceptance
|
||||
- **Planning:** ~1,000 words, 8 reasons, expenses, warm closing
|
||||
- **Final:** 3 short sections (ס' 84-86) — conclusion + 2 operational directives. No expenses. No warm closing.
|
||||
- **Lesson:** In partial acceptance, all reasoning is already in the discussion. Summary = operational directives only.
|
||||
|
||||
#### 8. Precedents — Planned vs. Actually Used
|
||||
| Planned but dropped | Added unexpectedly |
|
||||
|---|---|
|
||||
| חנין, נחמיאס (right to appeal) | ערר 1192/18 אילן (preservation + nuisance) |
|
||||
| הלכת שפר (deviation from plan) | ערר מובשוביץ 1009-02-24 (urban renewal — ~400 word quote) |
|
||||
| ערר כהן (no delaying permit) | ערר ארד 1156/18 (construction nuisance) |
|
||||
| עניין שיק (neighborhood change) | ערר זוהר 1169/19 (same topic) |
|
||||
|
||||
#### 9. New Transition Phrases Discovered
|
||||
- "הדברים משליכים על שיקול הדעת ב..." — linking finding to conclusion
|
||||
- "רוצה לומר כי" — alternative phrasing/explanation
|
||||
- "נוצר מצב בו" — presenting factual situation/problem
|
||||
- "לכך נוסיף כי" — adding another layer
|
||||
- "יש אולי להצר על כך ש..." — gentle critical remark
|
||||
- "עם ההבנה לטענה זו של העוררים, אין בידנו לקבלה" — soft acknowledge-reject
|
||||
|
||||
### Meta-Lesson
|
||||
Our skill was "over-indexed" on one case type (הכט = rejected appeal). The concentric circles model, threshold question as mandatory, and warm closing were all patterns from that single case. Beit HaKerem (partial acceptance) reveals that Dafna's approach is more flexible than we captured. We now have two data points — need to distinguish between patterns that are universal vs. result-dependent.
|
||||
|
||||
### Applied To
|
||||
- Updated `.claude/skills/legal-decision/SKILL.md` — added partial acceptance track (7.2, 7.3, 8.4), caveats to 6.1/6.3, new analytical patterns (6.10, 6.11), new golden ratios (3.2), new transition phrases (5.2)
|
||||
|
||||
---
|
||||
|
||||
## Lessons from קרית יערים-1 Structure Building (March 2026)
|
||||
|
||||
### Source
|
||||
- Structure draft: `01_Projects/כתיבת החלטות משפטיות/ערר קרית יערים-1/החלטה/החלטה-ערר-1130-25-מבנה.docx`
|
||||
- Reference decisions: בית הכרם (Dafna), ARAR-24-1078-44 (Arieli)
|
||||
- Date: March 2026
|
||||
|
||||
### 10. "Neutral Background" Rule
|
||||
- **Problem:** First draft put detailed 2017 district committee quotes ("נולד חטא", "חריג לסביבתו", "לא בדיוק המקום הזה") in the Background section.
|
||||
- **Chaim's correction:** These are parties' arguments disguised as background. The background was "revealing cards" before the parties spoke — effectively summarizing the case before presenting it.
|
||||
- **Rule:** Background (Block ו) = objective facts only. Test: "Does this sentence contain a direct quote from a party, or value/judgment words (חריג, חטא, בעייתי)?" If yes → belongs in Claims (Block ז) or Discussion (Block י), not Background. Prior decisions cited as dry fact ("rejected on date X") — reasoning, quotes, and interpretations appear only in claims/discussion.
|
||||
- **Applied to:** SKILL.md section 11.2 Block ו, added "⚠️ כלל רקע ניטרלי"
|
||||
|
||||
### 11. New 12-Block Decision Structure
|
||||
- Created formal 12-block structure based on analysis of Beit HaKerem + Arieli decisions
|
||||
- Added mandatory "pre-discussion draft" step (Block 12 in SKILL.md)
|
||||
- Created `create-decision-structure.cjs` script for generating structure DOCX
|
||||
- Key innovation from Arieli: "ההליכים בפני ועדת הערר" as separate section (Block ח)
|
||||
- "Judge Test": every block written as if administrative court judge reads cold
|
||||
|
||||
---
|
||||
|
||||
## Lessons from Systematic Corpus Analysis (24 decisions, April 2026)
|
||||
|
||||
### Source
|
||||
- All 24 proofread decisions in `/data/training/proofread/`
|
||||
- Full analysis: [`docs/corpus-analysis.md`](corpus-analysis.md)
|
||||
- Date: April 2026
|
||||
|
||||
### 12. System Learned Style but Not Substantive Content
|
||||
- **Problem:** Dafna reviewed Kiryat Yearim draft and noted missing planning discussion in block-yod
|
||||
- **Root cause:** The block-yod prompt taught CREAC methodology and "answer all claims" but never said "in licensing cases, include comprehensive planning discussion"
|
||||
- **Fix:** Content checklists added to `lessons.py` (`CONTENT_CHECKLISTS`), injected into block-yod prompt via `{content_checklist}`
|
||||
- **Applied to:** `lessons.py`, `block_writer.py`
|
||||
|
||||
### 13. Corpus Composition — All Licensing, No Betterment Levy
|
||||
- All 24 training decisions are licensing/construction (1xxx)
|
||||
- Zero betterment levy (8xxx) decisions in corpus
|
||||
- Not a current priority gap — focusing on licensing first
|
||||
|
||||
### 14. Planning Discussion Patterns in Licensing Decisions
|
||||
- **Always present** when the appeal reaches substantive planning questions
|
||||
- **Never present** when the appeal is purely jurisdictional or property-based
|
||||
- **Structure**: broad planning context → direct plan provision citations (200-600 words) → application to specific case → planning conclusion
|
||||
- **Deepest planning**: פרומר (pure plan interpretation), לבנון (height/building appendix), בית הכרם (multi-plan TAMA 38)
|
||||
- **No planning**: טלי-אביב (property only), גבאי (jurisdiction only)
|
||||
|
||||
### 15. Five Appeal Subtypes Identified (Not Just Three)
|
||||
Licensing appeals are not homogeneous — the discussion structure varies significantly:
|
||||
1. **Substantive licensing** — full planning discussion + legal analysis (majority of cases)
|
||||
2. **Threshold/jurisdiction** — legal analysis only, no planning
|
||||
3. **Property-focused** — תימוכין קנייניים, minimal planning
|
||||
4. **TAMA 38** — balancing public interest + planning + neighbor impact
|
||||
5. **Deviant use (שימוש חורג)** — deep plan interpretation across multiple plans
|
||||
|
||||
### 16. Chair Feedback System Established
|
||||
- DB table `chair_feedback` records Dafna's comments on drafts
|
||||
- Categories: missing_content, wrong_tone, wrong_structure, factual_error, style, other
|
||||
- MCP tools + UI page for recording and reviewing feedback
|
||||
- First entry: Kiryat Yearim — missing planning discussion (2026-04-12)
|
||||
39
docs/memory.md
Normal file
39
docs/memory.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Memory - Dafna Tamir Vault
|
||||
|
||||
## Project Context
|
||||
- This is an **Obsidian vault** for legal work - writing decisions for planning appeals committee (ועדת ערר לתכנון ובניה, מחוז ירושלים)
|
||||
- Chair: Adv. Dafna Tamir
|
||||
- Three areas: **היטל השבחה** (betterment levy), **רישוי ובנייה** (licensing/building permits), and **פיצויים** (compensation under section 197)
|
||||
|
||||
## Key Skills (2 active, consolidated 2026-02-07)
|
||||
- `legal-decision` - Main skill: style guide + analytical methodology + workflow. Updated 2026-03-21 with Beit HaKerem lessons: partial acceptance track (7.2/7.3/8.4), threshold question caveat (6.1), concentric circles flexibility (6.3), single-building analysis (6.10), master plan shield (6.11), new transition phrases, golden ratios for partial acceptance.
|
||||
- `legal-assistant` - For cataloging case files and creating timelines
|
||||
- Pipeline: **legal-assistant** (prep) → **legal-decision** (write)
|
||||
- 5 analysis skills archived in `.claude/skills/_archive/` (ניתוח-סגנון, ניתוח-רטוריקה, ניתוח-מבנה, גישה-שיפוטית, קטלוג-החלטות)
|
||||
- 4 duplicate/obsolete skills deleted (כותב-החלטות-תמיר, עוזר-כתיבת-החלטות, עוזר-תכנון-החלטות, docx-exporter)
|
||||
|
||||
## Critical Lessons Learned
|
||||
See [legal-decision-lessons.md](legal-decision-lessons.md) for full details (הכט + בית הכרם).
|
||||
|
||||
### From הכט 1180-1181 (rejected, 02.2026):
|
||||
1. **DO NOT use sub-headers in Discussion** - continuous essay
|
||||
2. **DO NOT split long citations** - 200-600 word blocks OK
|
||||
3. **Summary title is "סיכום"**
|
||||
4. **"Citation Through Consolidating Decision"** pattern
|
||||
|
||||
### From בית הכרם 1126/25 (partial acceptance, 03.2026):
|
||||
1. **Threshold question (ס' 152) is OPTIONAL** - skip when strong substantive issues exist
|
||||
2. **Concentric circles = rejected appeals only** — partial acceptance uses issue-by-issue analysis
|
||||
3. **New opening type: "tension mapping"** — list 4-6 tensions before analysis
|
||||
4. **"Single building" weakens TAMA 38 interest** — more cautious approval
|
||||
5. **Summary = ultra-minimal** (2-3 operational directives, no reasoning repetition, no expenses)
|
||||
|
||||
## Current/Next Projects
|
||||
- **Active:** ערר קרית יערים-1 (בתיקיית כתיבת החלטות משפטיות)
|
||||
|
||||
## Archived Projects
|
||||
- **Completed:** ערר הכט 1180-1181 (published 05.02.2026). דחייה.
|
||||
- **Completed:** ערר בית הכרם 1126/25 + 1141/25 תמ"א 38 (גרסה סופית - טיוטה 9, מרץ 2026). קבלה חלקית.
|
||||
- **Archived:** ערר 8107-25 אבו זאהריה (היטל השבחה) - archived 24.03.2026. החלטה מאחדת: ערר גפני.
|
||||
- **Archived:** ערר רמת שלמה 9005-24 (פיצויים ס' 197) - archived 24.03.2026. החלטה מאחדת: ערר ורדי 9003-23.
|
||||
- **Archived:** ערר רישוי 1184-25, ערר 1200-25, ערר 8070-25, ערר 1195-25 (archived 24.03.2026)
|
||||
50
docs/migration-plan.md
Normal file
50
docs/migration-plan.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Migration Plan — Dafna Vault → Nautilus
|
||||
|
||||
## Source
|
||||
- Obsidian vault at `/opt/apps/vaults/dafna-tamir/`
|
||||
- Claude memory at `/home/chaim/.claude/projects/-opt-apps-vaults-dafna-tamir/memory/`
|
||||
- 229MB compressed (excluding node_modules, .git)
|
||||
|
||||
## Target
|
||||
- PostgreSQL on Nautilus (legal-ai-postgres)
|
||||
- File storage on Nautilus
|
||||
- Gitea repository for code/scripts
|
||||
|
||||
## What to Migrate
|
||||
|
||||
### Knowledge (Priority 1 — enables RAG immediately)
|
||||
| Source | Target Table | Records |
|
||||
|--------|-------------|---------|
|
||||
| docs/legal-decision-lessons.md | lessons_learned | ~12 lessons |
|
||||
| SKILL.md section 5.2 | transition_phrases | ~30 phrases |
|
||||
| Published decisions (citations) | case_law | ~20 cases |
|
||||
| SKILL.md section 6.9 | statutory_provisions | ~10 statutes |
|
||||
|
||||
### Appeals (Priority 2)
|
||||
| Source | Target Table | Records |
|
||||
|--------|-------------|---------|
|
||||
| 01_Projects/*/README.md | appeals | 3 active |
|
||||
| 04_Archive/*/README.md | appeals | 14 archived |
|
||||
| All case headers | parties | ~50 |
|
||||
| All case headers | panels | ~17 |
|
||||
|
||||
### Documents (Priority 3)
|
||||
| Source | Target Table | Records |
|
||||
|--------|-------------|---------|
|
||||
| */חומרי-מקור/**/*.pdf | documents | ~200 PDFs |
|
||||
| */חומרי-מקור/**/*.md | documents | ~100 MDs |
|
||||
| */החלטה/*.docx | documents | ~10 DOCXs |
|
||||
|
||||
### Decisions (Priority 4)
|
||||
| Source | Target Table | Records |
|
||||
|--------|-------------|---------|
|
||||
| הכט published PDF | decisions + blocks + paragraphs | 1 complete |
|
||||
| בית הכרם Draft 9 | decisions + blocks + paragraphs | 1 complete |
|
||||
| קרית יערים draft | decisions + blocks | 1 in progress |
|
||||
|
||||
### Embeddings (Priority 5)
|
||||
| Source | Target Table | Records |
|
||||
|--------|-------------|---------|
|
||||
| All MD source docs | document_embeddings | ~500 chunks |
|
||||
| Decision paragraphs | paragraph_embeddings | ~300 paragraphs |
|
||||
| Case law summaries | case_law_embeddings | ~20 summaries |
|
||||
566
docs/product-specification.md
Normal file
566
docs/product-specification.md
Normal file
@@ -0,0 +1,566 @@
|
||||
# איפיון מוצר — עוזר משפטי
|
||||
|
||||
## מסמך זה
|
||||
מסמך איפיון מוצר (Product Specification) למערכת "עוזר משפטי" — כלי AI לכתיבת החלטות ועדת ערר לתכנון ובניה.
|
||||
|
||||
**מצב:** הושלם — עבר סקירת מומחה ותוקן
|
||||
**תאריך התחלה:** 2 באפריל 2026
|
||||
**בעל המוצר:** חיים מרכוס
|
||||
|
||||
---
|
||||
|
||||
## סעיף 0: בחירת מתודולוגיית איפיון
|
||||
|
||||
### השאלה
|
||||
מי המומחה המתאים להגדיר מוצר AI שמבצע עבודה מקצועית מורכבת (כתיבת החלטות משפטיות)?
|
||||
|
||||
### שתי הגישות
|
||||
|
||||
#### גישה א: מנהל מוצר (Product Manager)
|
||||
**מתמחה ב:** הגדרת "מה" — user stories, features, prioritization, go-to-market.
|
||||
**מתודולוגיות:** Lean Product, Jobs To Be Done (JTBD), Design Thinking.
|
||||
**חוזק:** מתרגם צורך עסקי למפרט טכני. מגדיר MVP. מתעדף features.
|
||||
**חולשה:** לא מתמחה בניתוח תהליכי עבודה מורכבים, לא מודד יעילות, לא מתכנן workflow אופטימלי.
|
||||
|
||||
#### גישה ב: מהנדס תעשייה וניהול (Industrial & Systems Engineer)
|
||||
**מתמחה ב:** הגדרת "איך" — ניתוח תהליכים, מדידת זמנים, זיהוי צווארי בקבוק, אופטימיזציה.
|
||||
**מתודולוגיות:** Systems Engineering (INCOSE SE Handbook), Business Process Modeling (BPMN), Value Stream Mapping, Human-Machine Teaming.
|
||||
**חוזק:** מפרק תהליך מורכב לשלבים מדידים. מזהה איפה AI מוסיף ערך ואיפה לא. מתכנן ממשק אדם-מכונה. מודד KPIs.
|
||||
**חולשה:** פחות מתמחה בחוויית משתמש (UX) ובתעדוף עסקי.
|
||||
|
||||
### ההמלצה: גישה משולבת עם דגש על הנדסת מערכות
|
||||
|
||||
**הנימוק:**
|
||||
המוצר שלנו הוא לא אפליקציה צרכנית אלא **כלי עבודה מקצועי** שמחליף/מסייע בתהליך מורכב. הבעיה המרכזית היא לא "אילו features לבנות" אלא **"איך דפנה עובדת ואיפה AI נכנס לתהליך"**. זו בדיוק ההתמחות של הנדסת תעשייה ומערכות.
|
||||
|
||||
**סימוכין אקדמיים:**
|
||||
|
||||
1. **INCOSE Systems Engineering Handbook (2023, 5th ed.)** — מגדיר שכלי AI מקצועי דורש קודם כל "Operational Concept" — תיאור מלא של תהליך העבודה לפני ואחרי הכנסת המערכת (Chapter 4.2: Stakeholder Needs and Requirements).
|
||||
|
||||
2. **Parasuraman, R., Sheridan, T.B., & Wickens, C.D. (2000). "A Model for Types and Levels of Human Interaction with Automation."** IEEE Transactions on Systems, Man, and Cybernetics. — מגדיר 10 רמות אוטומציה (LOA) בין "האדם עושה הכל" ל-"המכונה עושה הכל". המפתח: לכל שלב בתהליך צריך להגדיר את רמת האוטומציה הנכונה. לא הכל צריך להיות אוטומטי.
|
||||
|
||||
3. **Daugherty, P.R. & Wilson, H.J. (2018). "Human + Machine: Reimagining Work in the Age of AI."** Harvard Business Review Press. — מגדיר מודל "collaborative intelligence" שבו AI ואדם משלימים זה את זה. רלוונטי כי דפנה לא רוצה שה-AI יחליף אותה — היא רוצה שיעזור לה.
|
||||
|
||||
4. **Endsley, M.R. (2017). "From Here to Autonomy: Lessons Learned from Human-Automation Research."** Human Factors. — מזהיר מ-"automation complacency" — כשאנשים סומכים יותר מדי על AI ומפסיקים לבדוק. קריטי בהקשר משפטי שבו דפנה חייבת לקרוא ולאשר כל מילה.
|
||||
|
||||
**מסקנה:** נשתמש בגישת **Systems Engineering** כמסגרת ראשית, עם רכיבים מ-Product Management לתעדוף ו-MVP.
|
||||
|
||||
---
|
||||
|
||||
## טבלת מעקב איפיון
|
||||
|
||||
| סעיף | נושא | סטטוס | הערות |
|
||||
|------|------|-------|-------|
|
||||
| סעיף 0 | בחירת מתודולוגיה | ✅ מלא | הנדסת מערכות + ניהול מוצר |
|
||||
| סעיף 1 | חזון המוצר | ✅ מלא | בעיה, חזון, פתרון, משתמשים, מדדי הצלחה, scope |
|
||||
| סעיף 2 | בעלי עניין | ✅ מלא | חיים (מפעיל), דפנה (מגיהה+חותמת), שופט (מבחן עליון), חברי ועדה, מזכירות, צדדים |
|
||||
| סעיף 3 | תהליך עבודה נוכחי | ✅ מלא | נקודת כניסה, חומרים, תהליך, זמנים, צוואר בקבוק |
|
||||
| סעיף 4 | תהליך עבודה עתידי | ✅ מלא | 7 שלבים: קלט → עיבוד → תוצאה → סיעור מוחות (אם צריך) → כתיבה → פלט → למידה |
|
||||
| סעיף 5 | רמות אוטומציה | ✅ מלא | 3 רמות (אוטומטי/שיתופי/אנושי), מיפוי 11 שלבים, 7 עקרונות עיצוב, 4 סיכונים |
|
||||
| סעיף 6 | דרישות פונקציונליות | ✅ מלא | 40 דרישות ב-8 שלבים (כולל שלב 6 הגהת דפנה), כולן מבוססות על סעיפים 1-5 |
|
||||
| סעיף 7 | דרישות לא-פונקציונליות | ✅ מלא | 12 דרישות: שפה, ביצועים, דיוק, אבטחה, זמינות, ממשק |
|
||||
| סעיף 8 | גרסה מינימלית (MVP) | ✅ מלא | אין MVP — מוצר מלא בלבד. תוכנית הסמכה ב-4 שלבים |
|
||||
| סעיף 9 | מדדי הצלחה | ✅ מלא | 6 מדדים: אחוז שינוי, זמן, אפס הזיות, מענה לטענות, משקלות, ניטרליות |
|
||||
| סעיף 10 | סיכונים ומגבלות | ✅ מלא | 10 סיכונים עם מנגנוני הגנה, 4 מגבלות ידועות |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 1: חזון המוצר (Product Vision)
|
||||
|
||||
### טבלת מעקב שאלות — סעיף 1
|
||||
|
||||
| שאלה | סטטוס | תשובה |
|
||||
|------|-------|-------|
|
||||
| מה הבעיה שהמוצר פותר? | ✅ מלא | חיים (עו"ד, עוזר משפטי של דפנה) לא מצליח להתאים את סגנון הכתיבה שלו לסגנון של דפנה. צריך כלי AI שכותב בדיוק כמו דפנה |
|
||||
| מי המשתמש העיקרי? | ✅ מלא | חיים — מפעיל המערכת מהתחלה עד טיוטה סופית. דפנה רק מגיהה ומתקנת אחרי. הגרסה הסופית שדפנה מפיצה חוזרת למערכת ללמידה |
|
||||
| מה התוצר הסופי שיוצא מהמערכת? | ✅ מלא | קובץ DOCX מעוצב, כמעט מוכן לחתימה. עיצוב עברי RTL עם כותרות — skill קיים לייצור DOCX. נדרשות התאמות קטנות |
|
||||
| מה "הצלחה" נראית? | ✅ מלא | מצב תקין: דפנה משנה/מוסיפה עד 10% מהטקסט. הצלחה מלאה: 98% מהטקסט נשאר כמו שהמערכת כתבה |
|
||||
| מה מחוץ ל-scope? | ✅ מלא | המערכת רק כותבת החלטות. לא ניהול תיקים, לא תזמון, לא מיילים. מקבלת קבצים (PDF/DOCX/MD) ומסתמכת **רק** על מה שקיבלה — אסור לה להמציא מקורות או לצטט דברים שלא במסמכים |
|
||||
|
||||
### הבעיה
|
||||
דפנה תמיר, יו"ר ועדת ערר לתכנון ובניה מחוז ירושלים, נושאת עומס תיקים רב. חיים מרכוס הוא העוזר המשפטי שלה — עורך דין שמנסח עבורה טיוטות החלטות שדפנה עוברת עליהן, מתקנת, מגיהה ומתאימה לסגנונה לפני שהיא חותמת.
|
||||
|
||||
**הבעיה המרכזית:** לחיים יש סגנון כתיבה משפטי משלו שלא תואם את הסגנון הייחודי של דפנה. הפער בסגנון גורם לדפנה לבזבז זמן רב על תיקון והתאמה — בדיוק הזמן שהעוזר אמור לחסוך לה.
|
||||
|
||||
**מה שצריך:** כלי AI שמחליף את שלב הכתיבה הראשוני של חיים — כותב טיוטה ראשונית **בדיוק בסגנון של דפנה**, כך שדפנה מקבלת טיוטה שדורשת מינימום תיקונים.
|
||||
|
||||
**העיקרון:** מינימום זמן, מקסימום תוצר.
|
||||
|
||||
### משפט חזון
|
||||
כלי AI שכותב טיוטות החלטות ועדת ערר **בדיוק בסגנון של דפנה תמיר** — מקבל חומרי מקור ומוציא DOCX מעוצב כמעט מוכן לחתימה, כך שדפנה צריכה לשנות מינימום.
|
||||
|
||||
### הפתרון
|
||||
מערכת "עוזר משפטי" שמחליפה את שלב כתיבת הטיוטה הראשונית:
|
||||
|
||||
**קלט:** קבצים (PDF/DOCX/MD) מסוג כתבי בי-דין — ערר, תשובה, תגובה, פרוטוקול, תכנית, היתר, פסקי דין, החלטות.
|
||||
|
||||
**תהליך:** המערכת קוראת את החומר, מנתחת, ומנסחת החלטה בסגנון דפנה לפי ארכיטקטורת 12 בלוקים.
|
||||
|
||||
**פלט:** קובץ DOCX מעוצב — טיוטה כמעט מוכנה לחתימה.
|
||||
|
||||
**כלל ברזל:** המערכת מסתמכת **רק** על מסמכים שקיבלה בפועל. אסור לה להמציא מקורות, לצטט דברים שלא במסמכים, או להוסיף פסיקה שלא סופקה.
|
||||
|
||||
**לולאת למידה:** הגרסה הסופית שדפנה מפיצה (אחרי הגהה ותיקונים) חוזרת למערכת — כך המערכת לומדת מהפער בין הטיוטה שלה לגרסה הסופית ומשתפרת עם הזמן.
|
||||
|
||||
### משתמשים
|
||||
- **משתמש ראשי:** חיים מרכוס (עו"ד, עוזר משפטי) — מפעיל את המערכת מהתחלה עד טיוטה סופית
|
||||
- **משתמש עקיף:** דפנה תמיר (עו"ד, יו"ר ועדת ערר) — מקבלת את הטיוטה, מגיהה, מתקנת, חותמת
|
||||
|
||||
### מדדי הצלחה
|
||||
- **מצב תקין (target):** דפנה משנה/מוסיפה עד 10% מהטקסט
|
||||
- **הצלחה מלאה (stretch):** עד 5% שינוי
|
||||
|
||||
**הערה:** לפי מחקר Endsley (2017), מומחים בדרגת ההזדהות של דפנה כמעט תמיד ישנו ניסוחים — לא כי הם שגויים, אלא כי זו הדרך שלהם "לאמץ" את הטקסט. לכן יעד 2% לא ריאלי. יעד 5% מאפשר לדפנה מרחב אישי תוך שמירה על יעילות גבוהה. המדד יתכייל לאחר 5 החלטות ראשונות.
|
||||
|
||||
### מחוץ ל-scope
|
||||
- ניהול תיקים, תזמון דיונים, שליחת מיילים
|
||||
- חיפוש פסיקה באינטרנט — רק מה שסופק כמסמך
|
||||
- החלטה אוטונומית — דפנה תמיד קוראת, מגיהה וחותמת
|
||||
|
||||
---
|
||||
|
||||
## סעיף 2: בעלי עניין (Stakeholders)
|
||||
|
||||
### טבלת מעקב שאלות — סעיף 2
|
||||
|
||||
| שאלה | סטטוס | תשובה |
|
||||
|------|-------|-------|
|
||||
| מי מעורב בתהליך מלבד חיים ודפנה? | ✅ מלא | השופט — בעל עניין שקט. כל החלטה עלולה לעמוד לביקורת שיפוטית בעתמ"ם ובעליון |
|
||||
| מה השופט בודק? | ✅ מלא | הנמקה מלאה, תשתית עובדתית, סבירות ומידתיות, פרוצדורה תקינה, ציטוט מדויק |
|
||||
| האם יש עוד בעלי עניין? | ✅ מלא | מזכירות (מפיצה בלבד), חברי ועדה (דיון לפני הכתיבה, לא מעורבים בכתיבה), צדדים (רואים רק אחרי פרסום) |
|
||||
|
||||
### בעלי עניין
|
||||
|
||||
| תפקיד | שם | מעורבות | צורך עיקרי |
|
||||
|-------|-----|---------|-----------|
|
||||
| מפעיל המערכת | חיים מרכוס, עו"ד | מפעיל יום-יום — מהקמת תיק עד טיוטה סופית | טיוטה בסגנון דפנה, מינימום זמן, מקסימום תוצר |
|
||||
| יו"ר ועדת הערר | דפנה תמיר, עו"ד | מקבלת טיוטה, מגיהה, מתקנת, חותמת | טיוטה שדורשת מינימום שינויים (יעד: עד 10%) |
|
||||
| **בעל עניין שקט: השופט** | שופט בית משפט מנהלי | לא משתמש במערכת, אבל כל החלטה חייבת לעמוד בביקורתו | — |
|
||||
|
||||
### "מבחן השופט" — הדרישה העליונה של המוצר
|
||||
|
||||
כל החלטה שהמערכת מייצרת חייבת לעמוד בביקורת שיפוטית של שופט בית משפט לעניינים מנהליים (ובערעור — בית המשפט העליון). בפועל השופט בודק:
|
||||
|
||||
1. **הנמקה מלאה** — כל טענה שהועלתה קיבלה מענה. התעלמות מטענה = עילת ביטול.
|
||||
2. **תשתית עובדתית** — העובדות מוצגות נכון, מלאות, לא מוטות. רקע לא ניטרלי = חשש למשוא פנים.
|
||||
3. **סבירות ומידתיות** — ההכרעה סבירה לאור הטענות והעובדות. לא מספיק "נדחה" — צריך להסביר למה ולמה זה מידתי.
|
||||
4. **פרוצדורה תקינה** — כל הצדדים קיבלו הזדמנות להשמיע קולם. דיון התקיים. הזדמנויות לטעון ניתנו.
|
||||
5. **ציטוט מדויק** — כל הפניה לפסיקה, חקיקה או מסמך חייבת להיות מדויקת ומבוססת על מה שסופק בפועל.
|
||||
|
||||
**זו לא דרישה סגנונית — זו הדרישה העליונה של המוצר.**
|
||||
|
||||
### בעלי עניין נוספים (לא משתמשים במערכת)
|
||||
|
||||
| תפקיד | מעורבות | השלכה על המוצר |
|
||||
|-------|---------|---------------|
|
||||
| חברי ועדת הערר | דיון פרונטלי לפני הכתיבה — לא מעורבים בכתיבה עצמה | המערכת צריכה לקלוט את פרוטוקול הדיון כקלט |
|
||||
| מזכירות הוועדה | מפיצה את ההחלטה הסופית בלבד | אין השלכה על המוצר |
|
||||
| הצדדים (עוררים/משיבים) | רואים את ההחלטה רק אחרי פרסום | אין השלכה ישירה, אבל הם אלה שעלולים לעתור — חוזר ל"מבחן השופט" |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 3: תהליך העבודה הנוכחי (As-Is Process)
|
||||
|
||||
### טבלת מעקב שאלות — סעיף 3
|
||||
|
||||
| שאלה | סטטוס | תשובה |
|
||||
|------|-------|-------|
|
||||
| מתי המערכת נכנסת לתמונה? | ✅ מלא | אחרי שהדיון הסתיים — כל הצדדים טענו בכתב ובעל פה, הוועדה דנה, ועכשיו צריך לכתוב החלטה |
|
||||
| מה החומרים שעל השולחן ברגע הזה? | ✅ מלא | כל מסמך שנמסר לוועדה: כתב ערר, כתב תשובה (ועדה מקומית + משיבים), פרוטוקולים, השלמות טיעון |
|
||||
| מה אתה (חיים) עושה היום צעד אחרי צעד כשאתה כותב טיוטה? | ✅ מלא | קורא את כל החומר → כותב פתח דבר/רקע → ממשיך צעד אחרי צעד. סדר הפרקים משתנה לפי סוג ההחלטה |
|
||||
| כמה זמן התהליך לוקח היום? | ✅ מלא | שבוע לטיוטה שלמה. יעד: יום אחד |
|
||||
| מה הכי קשה / לוקח הכי הרבה זמן? | ✅ מלא | סיכום הטענות והדיון. בעיקר הדיון (בלוק י) — הוא צוואר הבקבוק |
|
||||
|
||||
### נקודת הכניסה
|
||||
המערכת נכנסת לתמונה **אחרי** שכל השלבים הבאים הסתיימו:
|
||||
- כתבי הערר הוגשו ✅
|
||||
- כתבי תשובה/תגובה הוגשו ✅
|
||||
- השלמות טיעון (אם היו) הוגשו ✅
|
||||
- דיון פרונטלי בוועדת הערר התקיים ✅
|
||||
- הוועדה דנה פנימית והחליטה על הכיוון ✅
|
||||
|
||||
**עכשיו** — צריך לכתוב את ההחלטה. כאן המערכת נכנסת.
|
||||
|
||||
### תהליך העבודה הנוכחי (ללא המערכת)
|
||||
|
||||
1. **קריאת כל החומר** — כתבי ערר, תשובות, פרוטוקולים, השלמות
|
||||
2. **כתיבת פתיחה ורקע** (בלוקים ה-ו) — הגדרות, עובדות, תכניות
|
||||
3. **סיכום טענות** (בלוק ז) — לכל צד בנפרד — **לוקח הרבה זמן**
|
||||
4. **הליכים** (בלוק ח) — אם היו סיור/השלמות/החלטות ביניים
|
||||
5. **דיון** (בלוק י) — **צוואר הבקבוק** — ניתוח משפטי, פסיקה, יישום, הכרעה
|
||||
6. **סיכום** (בלוק יא) — הוראות אופרטיביות
|
||||
7. **שליחה לדפנה** — דפנה מגיהה, מתקנת, חותמת
|
||||
|
||||
**זמן נוכחי:** שבוע לטיוטה שלמה
|
||||
**יעד:** יום אחד
|
||||
|
||||
### ניתוח מבנה 3 החלטות — ממצאים
|
||||
|
||||
מניתוח הכט (דחייה), בית הכרם (קבלה חלקית), אריאלי (קבלה) עולה:
|
||||
|
||||
**בלוקים קבועים (תמיד קיימים):**
|
||||
- ה — פתיחה (אבל בניסוחים שונים: "לפנינו" / "עניינה של החלטה זו")
|
||||
- ו — רקע / "פתח דבר" (היקף משתנה: 3% עד 18%)
|
||||
- ז — טענות הצדדים
|
||||
- י — דיון והכרעה
|
||||
- יא — סיכום / סוף דבר
|
||||
- יב — חתימות
|
||||
|
||||
**בלוקים מותנים (תלויים בתיק):**
|
||||
- ח — הליכים: קיים כשהיו הליכים מורכבים (דיון + סיור + השלמות רבות). באריאלי = 27% מההחלטה
|
||||
- ט — תכניות/מסגרת נורמטיבית: קיים כשיש מורכבות תכנונית או שאלה משפטית (ס' 152). בהכט = 32%
|
||||
|
||||
**⚠ ממצא טכני:** ה-parser הנוכחי לא זיהה את בלוקים ה ו-ו של אריאלי כי הפתיחה שלה ("עניינה של החלטה זו") שונה מ-"לפנינו", וכותרת הרקע ("פתח דבר") לא הייתה בדפוסי החיפוש. דורש תיקון.
|
||||
|
||||
---
|
||||
|
||||
## סעיף 4: תהליך העבודה העתידי (To-Be Process)
|
||||
|
||||
### טבלת מעקב שאלות — סעיף 4
|
||||
|
||||
| שאלה | סטטוס | תשובה |
|
||||
|------|-------|-------|
|
||||
| איך אתה רואה את התהליך עם המערכת? | ✅ מלא | מינימום ממשק, אבל חייב man-in-the-middle לפני הדיון |
|
||||
| מה השלב שחייב אדם? | ✅ מלא | הזנת התוצאה שדפנה קבעה (דחייה/קבלה/חלקית) — לפני שהמערכת כותבת את הדיון |
|
||||
| מי קובע את התוצאה? | ✅ מלא | דפנה — היא השופטת. היא מעבירה לחיים את ההחלטה |
|
||||
| מה דפנה מעבירה? | ✅ מלא | לא קבוע — לפעמים רק "נדחה/התקבל", לפעמים מנומק יותר |
|
||||
| מה קורה עם הנימוקים של דפנה? | ✅ מלא | אם דפנה נותנת נימוק — ישר לכתיבה. אם רק תוצאה — סיעור מוחות בין חיים למערכת על בסיס החומר המשפטי כדי לגבש את הכיוון. לא מתחילים לכתוב דיון לפני שיש כיוון מדויק |
|
||||
|
||||
### תהליך העבודה העתידי
|
||||
|
||||
**שלב 1 — קלט (חיים)**
|
||||
חיים מעלה למערכת את כל המסמכים שנמסרו לוועדה (PDF/DOCX/MD).
|
||||
|
||||
**שלב 2 — עיבוד אוטומטי (מערכת)**
|
||||
המערכת קוראת את כל החומר, מזהה צדדים, מסווגת מסמכים, מחלצת טענות.
|
||||
|
||||
**שלב 3 — הזנת תוצאה (חיים) ← man-in-the-middle**
|
||||
חיים מזין את התוצאה שדפנה קבעה:
|
||||
- סוג: דחייה / קבלה / קבלה חלקית
|
||||
- נימוק: אם דפנה נתנה → ישר לשלב 4ב
|
||||
|
||||
**שלב 4א — סיעור מוחות (חיים + מערכת) ← רק אם אין נימוק**
|
||||
אם דפנה נתנה רק תוצאה בלי נימוק — המערכת וחיים מנהלים שיח:
|
||||
- המערכת מציגה את הטענות המרכזיות מהחומר
|
||||
- חיים והמערכת דנים על בסיס מה מגיעים לתוצאה
|
||||
- **לא מתחילים לכתוב דיון לפני שיש כיוון מדויק**
|
||||
- התוצר: מסמך כיוון קצר — מה הנימוקים המרכזיים, באיזה סדר, מה הפסיקה הרלוונטית
|
||||
|
||||
**שלב 4ב — כתיבת טיוטה (מערכת)**
|
||||
המערכת כותבת את ההחלטה בלוק אחרי בלוק, בסגנון דפנה, לפי התוצאה והכיוון שנקבעו.
|
||||
|
||||
**שלב 5 — פלט (חיים)**
|
||||
חיים מקבל DOCX מעוצב — טיוטה כמעט מוכנה.
|
||||
|
||||
**שלב 6 — הגהה ותיקונים (דפנה)**
|
||||
דפנה קוראת, מתקנת, מגיהה, חותמת.
|
||||
|
||||
**שלב 7 — לולאת למידה (מערכת)**
|
||||
הגרסה הסופית שדפנה מפיצה חוזרת למערכת — המערכת לומדת מהפער.
|
||||
|
||||
---
|
||||
|
||||
## סעיף 5: רמות אוטומציה (Levels of Automation)
|
||||
|
||||
### בסיס אקדמי
|
||||
|
||||
הניתוח מבוסס על 9 מקורות אקדמיים (ביבליוגרפיה מלאה בנספח):
|
||||
|
||||
| מקור | תרומה |
|
||||
|------|-------|
|
||||
| פרסורמן, שרידן וויקנס (2000) | מודל 10 רמות אוטומציה × 4 שלבי עיבוד מידע |
|
||||
| אנדסלי (2017) | סיכוני שאננות אוטומציה — דווקא מומחים רגישים יותר |
|
||||
| קאמינגס (2004) | הטיית אוטומציה — commission errors ו-omission errors |
|
||||
| סורדין (2018) | שלוש רמות AI במשפט: תומך / מחליף / משבש |
|
||||
| רילינג (2020) | הבחנה בין פרוצדורלי (אוטומטי) לשיקול דעת (אנושי) |
|
||||
| CEPEJ (2018) | חמישה עקרונות אתיים ל-AI בשיפוט — "under user control" |
|
||||
| INCOSE (2023) | הקצאת פונקציות דינמית — לפי מורכבות, סיכון, עומס |
|
||||
|
||||
### שלוש רמות אוטומציה
|
||||
|
||||
| רמה | שם | תיאור | LOA (פרסורמן) |
|
||||
|------|-----|-------|--------------|
|
||||
| **א — אוטומטי** | המערכת מבצעת, מדווחת לאדם | 7-9 |
|
||||
| **ב — שיתופי** | המערכת מנסחת טיוטה, האדם מאשר/עורך/דוחה | 4-5 |
|
||||
| **ג — אנושי** | האדם מבצע, המערכת מספקת מידע בלבד | 1-3 |
|
||||
|
||||
### מיפוי דו-ממדי: רמות אוטומציה × שלבי עיבוד מידע (Parasuraman 2000)
|
||||
|
||||
| שלב עבודה | שלב עיבוד (Parasuraman) | רמה |
|
||||
|-----------|------------------------|-----|
|
||||
| קריאת מסמכים וסיווג | Information Acquisition | א — אוטומטי |
|
||||
| חילוץ טענות | Information Acquisition + Analysis | ב — שיתופי |
|
||||
| חיפוש תקדימים | Information Analysis | ב — שיתופי |
|
||||
| גיבוש נימוקים | Information Analysis + Decision Selection | ב — שיתופי |
|
||||
| **קביעת תוצאה** | **Decision Selection** | **ג — אנושי** |
|
||||
| כתיבת בלוקים ה-ח | Action Implementation | ב — שיתופי |
|
||||
| כתיבת דיון (י) | Action Implementation (high-stakes) | ב — שיתופי |
|
||||
| ייצוא DOCX | Action Implementation (low-stakes) | א — אוטומטי |
|
||||
|
||||
### מיפוי שלבי העבודה לרמות אוטומציה
|
||||
|
||||
| שלב | תוכן | רמה | נימוק |
|
||||
|-----|-------|-----|-------|
|
||||
| קריאת מסמכים וסיווג | זיהוי סוג מסמך, חילוץ מטא-דאטה | **א — אוטומטי** | פרוצדורלי, ניתן לביקורת, סיכון נמוך |
|
||||
| חילוץ טענות | סיכום טענות מכתבי טענות | **ב — שיתופי** | דורש נאמנות למקור — AI מנסח, אדם מוודא |
|
||||
| כתיבת רקע (בלוק ו) | עובדות, תכניות | **ב — שיתופי** | חובת ניטרליות — AI מנסח, אדם בודק |
|
||||
| כתיבת טענות (בלוק ז) | סיכום טענות בגוף שלישי | **ב — שיתופי** | נאמנות למקור |
|
||||
| כתיבת הליכים (בלוק ח) | תיעוד כרונולוגי | **ב — שיתופי** | בעיקר עובדתי אבל דורש דיוק |
|
||||
| חיפוש תקדימים | RAG — מציאת פסיקה דומה | **ב — שיתופי** | AI מציע 3-5 חלופות, אדם בוחר (קאמינגס) |
|
||||
| **קביעת תוצאה** | דחייה/קבלה/חלקית | **ג — אנושי בלבד** | **החלטה שיפוטית — דפנה בלבד** |
|
||||
| **גיבוש נימוקים** | סיעור מוחות על הכיוון | **ב — שיתופי** | AI מציג טענות ומציע כיוונים, חיים מחליט. אופציונלי גם כשיש נימוק |
|
||||
| כתיבת דיון (בלוק י) | ניתוח משפטי, CREAC | **ב — שיתופי** | AI מנסח על בסיס הכיוון שנקבע, אדם עורך |
|
||||
| כתיבת סיכום (בלוק יא) | הוראות אופרטיביות | **ב — שיתופי** | נגזר מהדיון, אבל חייב לשקף הכרעה מדויקת |
|
||||
| ייצוא DOCX | עיצוב מסמך | **א — אוטומטי** | טכני לחלוטין |
|
||||
|
||||
### עקרונות עיצוב (מבוססי מחקר)
|
||||
|
||||
| עיקרון | מקור | יישום |
|
||||
|--------|------|-------|
|
||||
| **"אדם בלולאה"** | אנדסלי (2017) | דפנה קובעת תוצאה, חיים מאשר כיוון — לפני שהמערכת כותבת |
|
||||
| **שקיפות** | אנדסלי (2017), CEPEJ (2018) | כל טיוטה מציגה מקורות. רמת ודאות ליד תקדימים |
|
||||
| **מעורבות אקטיבית** | אנדסלי (2017) | חובת מעבר מבלוק לבלוק — אין "ייצר הכל" בלחיצה |
|
||||
| **הצגת חלופות** | קאמינגס (2004) | חיפוש תקדימים מחזיר 3-5 חלופות, לא "התקדים הנכון" |
|
||||
| **כפייה קוגניטיבית** | קאמינגס (2004) | חיים מזין כיוון **לפני** שרואה טיוטת דיון |
|
||||
| **אחריותיות** | CEPEJ (2018) | החלטה נושאת חתימת דפנה. תיעוד שנעשה שימוש ב-AI |
|
||||
| **הקצאה דינמית** | INCOSE (2023) | ככל שהמשימה קרובה יותר להכרעה — פחות אוטומציה |
|
||||
|
||||
### סיכונים שזוהו
|
||||
|
||||
| סיכון | מקור | מנגנון הגנה |
|
||||
|-------|------|------------|
|
||||
| **שאננות אוטומציה** — דפנה מפסיקה לבדוק טיוטות | אנדסלי | לולאת למידה: השוואת טיוטה לגרסה סופית מודדת כמה דפנה שינתה |
|
||||
| **הטיית אוטומציה** — חיים מאמץ תקדים שגוי | קאמינגס | כלל ברזל: רק מה שסופק כמסמך. + הצגת חלופות |
|
||||
| **שחיקת מיומנות** — חיים מפסיק ללמוד לכתוב | פרסורמן | סיעור מוחות חובה לפני כל דיון |
|
||||
| **לולאת חיזוק** — החלטות עתידיות ידמו לעבר | אלטרס (2016) | כל החלטה חדשה מבוססת על חומרי המקור, לא על תבנית |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 6: דרישות פונקציונליות
|
||||
|
||||
### טבלת מעקב שאלות — סעיף 6
|
||||
|
||||
| שאלה | סטטוס | תשובה |
|
||||
|------|-------|-------|
|
||||
| מה פורמטי הקלט? | ✅ מלא | PDF, DOCX, MD — כל מסמך שנמסר לוועדה (מסעיף 1) |
|
||||
| מה סוגי המסמכים? | ✅ מלא | ערר, תשובה, תגובה, פרוטוקול, תכנית, היתר, פסקי דין, החלטות (מסעיף 1) |
|
||||
| מה הפלט? | ✅ מלא | DOCX מעוצב RTL, כמעט מוכן לחתימה (מסעיף 1) |
|
||||
| מה סוגי הערר? | ✅ מלא | רישוי (1xxx), היטל השבחה (8xxx), פיצויים (9xxx) — סגנון שונה לכל אחד |
|
||||
| מה שלבי התהליך? | ✅ מלא | 7 שלבים כולל man-in-the-middle (מסעיף 4) |
|
||||
| מה רמות האוטומציה? | ✅ מלא | 3 רמות: אוטומטי/שיתופי/אנושי (מסעיף 5) |
|
||||
|
||||
### דרישות פונקציונליות — לפי שלבי התהליך
|
||||
|
||||
#### שלב 1: קלט מסמכים
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ק-1 | המערכת מקבלת קבצי PDF, DOCX, MD | חובה |
|
||||
| ק-2 | המערכת מחלצת טקסט מלא מכל מסמך (כולל OCR לסרוקים) | חובה |
|
||||
| ק-3 | המערכת מסווגת כל מסמך לסוג (ערר/תשובה/פרוטוקול וכו') | חובה |
|
||||
| ק-4 | המערכת מזהה את הצדדים (עוררים, משיבים, ועדה, מבקשי היתר) | חובה |
|
||||
| ק-5 | המערכת מזהה את סוג הערר (רישוי/השבחה/פיצויים) לפי מספר התיק | חובה |
|
||||
| ק-6 | המערכת מודדת גודל כל החומרים בטוקנים ומתריעה אם חורגים מ-80% של context window | חובה |
|
||||
| ק-7 | כשחומרים חורגים — המערכת מפעילה אסטרטגיית סיכום/חלוקה עם סדר עדיפויות (ערר ותשובה קודם לנספחים) | חובה |
|
||||
| ק-8 | **הגנת prompt injection** — הפרדה בין הוראות מערכת לתוכן מסמכים. סניטיזציה של קלט ממסמכי צדדים חיצוניים | חובה |
|
||||
| ק-9 | **איחוד תיקים** — המערכת תומכת בתיק שמאחד כמה עררים (כמו 1078+1083). בלוק ה מגדיר כמה מספרי תיק, בלוק ז מכסה טענות של כל העוררים | רצוי |
|
||||
|
||||
#### שלב 2: עיבוד וניתוח
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ע-1 | המערכת מחלצת טענות מכתבי טענות — לפי צד | חובה |
|
||||
| ע-2 | המערכת מזהה תכניות חלות על המקרקעין | חובה |
|
||||
| ע-3 | המערכת מזהה פסיקה שמצוטטת במסמכים | חובה |
|
||||
| ע-4 | המערכת מציגה סיכום חומרים לחיים לפני כתיבה | חובה |
|
||||
|
||||
#### שלב 3: הזנת תוצאה (man-in-the-middle)
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ת-1 | חיים מזין את התוצאה שדפנה קבעה: דחייה / קבלה / קבלה חלקית | חובה |
|
||||
| ת-2 | חיים מזין נימוק (אם דפנה נתנה) — טקסט חופשי | חובה |
|
||||
| ת-3 | אם אין נימוק — המערכת מפעילה שלב סיעור מוחות (שלב 4א). גם אם יש נימוק — חיים יכול לבקש סיעור מוחות כאופציה | חובה |
|
||||
|
||||
#### שלב 4א: סיעור מוחות (כשאין נימוק)
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ס-1 | המערכת מציגה את הטענות המרכזיות מהחומר | חובה |
|
||||
| ס-2 | המערכת מציעה 2-3 כיוונים אפשריים לנימוק (לא המלצה אחת) | חובה |
|
||||
| ס-3 | חיים והמערכת מנהלים שיח עד שמגיעים לכיוון מוסכם | חובה |
|
||||
| ס-4 | התוצר: מסמך כיוון — נימוקים מרכזיים, סדר, פסיקה רלוונטית | חובה |
|
||||
| ס-5 | **לא מתחילים לכתוב דיון לפני שיש כיוון מאושר** | חובה |
|
||||
|
||||
#### שלב 4ב: כתיבת טיוטה
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| כ-0 | המערכת ממלאת אוטומטית בלוקים א-ד (כותרת, הרכב, צדדים, "החלטה") ויב (חתימות) ממטא-דאטה של התיק | חובה |
|
||||
| כ-1 | המערכת כותבת בלוק אחרי בלוק לפי סדר: ה → ו → ז → ח → ט → י → יא | חובה |
|
||||
| כ-2 | כל בלוק נכתב בסגנון דפנה — טון, ביטויי מעבר, מבנה | חובה |
|
||||
| כ-3 | סגנון הכתיבה מותאם לסוג הערר (חם לרישוי, קר להשבחה) | חובה |
|
||||
| כ-4 | בלוק ח נכתב רק אם היו הליכים מעבר לדיון פשוט (סיור/השלמות) | חובה |
|
||||
| כ-5 | בלוק ט — רישום תכניות תמיד. פרק נפרד רק כשמורכבות תכנונית מצדיקה | חובה |
|
||||
| כ-6 | בלוק י — CREAC: מסקנה בפתיחה, כלל, הסבר, יישום, מסקנה | חובה |
|
||||
| כ-7 | בלוק י — מענה לכל טענה שהוצגה בבלוק ז | חובה |
|
||||
| כ-8 | **כלל ברזל: המערכת מצטטת רק מה שסופק כמסמך** | חובה |
|
||||
| כ-9 | רקע ניטרלי (בלוק ו) — ניטרליות לקסיקלית (אין מילות שיפוט) **וגם** מבנית (סדר הצגת עובדות מאוזן, אורך יחסי של סעיפים לא מטה, בחירת עובדות לא סלקטיבית) | חובה |
|
||||
| כ-10 | ללא כפילות (בלוק י) — הפניות לבלוקים קודמים, לא חזרה | חובה |
|
||||
| כ-11 | משקלות בלוקים לפי יחסי הזהב (סוג ערר × תוצאה) | חובה |
|
||||
| כ-12 | **שמירת מצב ביניים** — אחרי כל בלוק שנכתב, המצב נשמר ב-DB. חיים יכול להפסיק ולהמשיך מחר | חובה |
|
||||
| כ-13 | **recovery** — אם המערכת נופלת, חיים ממשיך מהבלוק האחרון שנשמר | חובה |
|
||||
| כ-14 | **חזרה אחורה** — חיים יכול לחזור לבלוק קודם ולכתוב אותו מחדש. בלוקים תלויים מתעדכנים בהתאם | חובה |
|
||||
| כ-15 | **ניהול גרסאות** — כל בלוק שומר היסטוריית גרסאות. חיים יכול לחזור לגרסה קודמת של בלוק ספציפי | רצוי |
|
||||
|
||||
#### שלב 5: פלט
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| פ-0 | **בדיקת QA אוטומטית לפני ייצוא** — ולידציה של: כל הפניה מוולדת (grounding), כל טענה נענתה, רקע ניטרלי, משקלות בטווח, מספור רציף. אם נכשל — לא מייצא, מציג דוח שגיאות | חובה |
|
||||
| פ-1 | ייצוא DOCX מעוצב — גופן David, RTL, כותרות, מספור סעיפים רציף | חובה |
|
||||
| פ-2 | מקומות תמונה מסומנים (GIS, תשריט, סיור) | רצוי |
|
||||
| פ-3 | הגדרות "להלן" מופיעות לפני השימוש הראשון | חובה |
|
||||
|
||||
#### שלב 6: הגהת דפנה ותיקונים
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ה-1 | חיים שולח את ה-DOCX לדפנה (מייל / שיתוף קובץ — מחוץ למערכת) | חובה |
|
||||
| ה-2 | דפנה מגיהה ומתקנת ב-Word — עם track changes | חובה |
|
||||
| ה-3 | חיים מעלה את הגרסה הסופית (DOCX שדפנה חתמה) בחזרה למערכת | חובה |
|
||||
| ה-4 | המערכת מזהה שזו גרסה סופית (לא טיוטה) ומפעילה את לולאת הלמידה | חובה |
|
||||
| ה-5 | שמירת הגרסה הסופית ב-DB עם קישור לטיוטה המקורית | חובה |
|
||||
|
||||
#### שלב 7: לולאת למידה
|
||||
|
||||
| מזהה | דרישה | עדיפות |
|
||||
|------|-------|--------|
|
||||
| ל-1 | קליטת גרסה סופית (שדפנה חתמה) בחזרה למערכת | חובה |
|
||||
| ל-2 | השוואת טיוטה לגרסה סופית — זיהוי מה שונה | חובה |
|
||||
| ל-3 | חילוץ לקחים — ביטויי מעבר חדשים, דפוסי כתיבה שהשתנו, שגיאות חוזרות | חובה |
|
||||
| ל-4 | עדכון בסיס הידע: הוספת ביטויים חדשים ל-transition_phrases, עדכון יחסי זהב, עדכון דוגמאות ב-RAG | חובה |
|
||||
| ל-5 | **מנגנון עדכון:** לא fine-tuning אלא עדכון RAG index + few-shot examples + prompt engineering. הגרסה הסופית הופכת לדוגמה שה-prompt מפנה אליה | חובה |
|
||||
| ל-6 | מנגנון rollback — אם עדכון מדרדר איכות (אחוז שינוי עולה), חזרה למצב קודם | חובה |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 7: דרישות לא-פונקציונליות
|
||||
|
||||
| מזהה | קטגוריה | דרישה | עדיפות |
|
||||
|------|---------|-------|--------|
|
||||
| לפ-1 | שפה | כל הממשק, הפלט והשיח בעברית | חובה |
|
||||
| לפ-2 | שפה | תמיכה מלאה ב-RTL — בממשק, ב-DOCX, ובטבלאות | חובה |
|
||||
| לפ-3 | ביצועים | טיוטה שלמה תוך שעות (לא ימים) — יעד: יום עבודה אחד כולל סיעור מוחות | חובה |
|
||||
| לפ-4 | ביצועים | חיפוש תקדימים (RAG) — תשובה תוך 10 שניות | רצוי |
|
||||
| לפ-4א | ביצועים | כתיבת בלוק בודד — עד 5 דקות לבלוקים ה-ט, עד 15 דקות לבלוק י (opus + thinking) | רצוי |
|
||||
| לפ-4ב | ביצועים | כתיבה אסינכרונית — חיים מפעיל כתיבת בלוק וממשיך לעבוד. התראה כשהבלוק מוכן | רצוי |
|
||||
| לפ-5 | דיוק | **מנגנון grounding** — כל הפניה לפסיקה/חקיקה/מסמך מקושרת למסמך מקור ספציפי עם citation tracking | חובה |
|
||||
| לפ-5א | דיוק | **ולידציה אוטומטית** — כל ציטוט/הפניה נבדק מול המסמכים שסופקו. הפניה שלא עוברת ולידציה = נחסמת (לא נכנסת לטיוטה) | חובה |
|
||||
| לפ-5ב | דיוק | **מדד: 0% הפניות לא מוולדות** — לא שאין הזיות, אלא שכל הזיה נתפסת לפני שנכנסת לטיוטה | חובה |
|
||||
| לפ-6 | דיוק | ציטוטים — נאמנות מוחלטת למקור. לא לשנות מילים, לא לקצר בלי לציין | חובה |
|
||||
| לפ-7 | אבטחה | חומרי התיקים חסויים — לא נשלחים לשירותים חיצוניים מלבד Anthropic API | חובה |
|
||||
| לפ-8 | אבטחה | ניהול סודות דרך Infisical — לא hardcoded | חובה |
|
||||
| לפ-9 | זמינות | המערכת רצה על שרת Nautilus — זמינה 24/7 | רצוי |
|
||||
| לפ-10 | תחזוקה | גיבוי DB יומי אוטומטי | חובה |
|
||||
| לפ-11 | ממשק | מינימום ממשק — עבודה דרך Claude Code (CLI), לא דרך web UI | חובה |
|
||||
| לפ-12 | מעקב | כל פעולה מתועדת — מי הזין, מתי, מה שונה | רצוי |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 8: MVP — גרסה מינימלית
|
||||
|
||||
### אין MVP — מוצר מלא בלבד
|
||||
|
||||
**אין גרסה מצומצמת.** כל הדרישות הפונקציונליות והלא-פונקציונליות הכרחיות. המערכת עובדת במלואה או לא עובדת.
|
||||
|
||||
**הנימוק:** המוצר פועל בסביבת אמת — החלטות משפטיות שעומדות לביקורת שיפוטית. טעות (ציטוט שגוי, טענה שלא נענתה, רקע לא ניטרלי) היא לא באג שאפשר לתקן בגרסה הבאה — היא עלולה להגיע לבית המשפט העליון.
|
||||
|
||||
### מגבלת scope גרסה ראשונה
|
||||
|
||||
**הגרסה הראשונה מכסה רישוי ובנייה (1xxx) והיטל השבחה (8xxx) בלבד.**
|
||||
|
||||
אין נתוני אימון לפיצויים (9xxx) — המערכת לא תקבל תיקי פיצויים עד שנלמד מהחלטות מהסוג הזה. כשתיק פיצויים מוזן — המערכת מתריעה ומסרבת לכתוב.
|
||||
|
||||
**קריטריונים להרחבה לפיצויים:**
|
||||
- לפחות 3 החלטות סופיות מסוג 9xxx נקלטו ופורקו
|
||||
- parser מכויל לסוג הזה
|
||||
- יחסי זהב מוגדרים
|
||||
|
||||
### תוכנית השקה — לא MVP אלא מבחן הסמכה
|
||||
|
||||
במקום "MVP → שיפור" — תהליך של **מבחן הסמכה** לפני שימוש אמיתי:
|
||||
|
||||
| שלב | תוכן | קריטריון מעבר |
|
||||
|-----|-------|--------------|
|
||||
| שלב א — פיתוח | בניית כל 42 הדרישות | כל הדרישות ממומשות |
|
||||
| שלב ב — מבחן על תיק שהושלם | המערכת כותבת החלטה לתיק שכבר יש לו החלטה סופית (למשל הכט) | השוואת הטיוטה להחלטה הסופית — פער קטן מ-10% |
|
||||
| שלב ג — מבחן על תיק חי | המערכת כותבת טיוטה לתיק אמיתי שדפנה טרם כתבה | דפנה בודקת — אם שינתה פחות מ-10% → עובר |
|
||||
| שלב ד — שימוש מבצעי | שימוש שוטף עם לולאת למידה | יעד: 98% ללא שינוי |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 9: מדדי הצלחה (KPIs)
|
||||
|
||||
| מדד | תקין | הצלחה | מדידה |
|
||||
|-----|------|-------|-------|
|
||||
| **אחוז שינוי** — כמה דפנה משנה מהטיוטה | עד 10% | עד 5% | ראה הגדרה מתמטית למטה. יתכייל אחרי 5 החלטות |
|
||||
|
||||
### הגדרה מתמטית: אחוז שינוי
|
||||
|
||||
**שיטת מדידה:** word-level edit distance (Levenshtein על מילים, לא תווים)
|
||||
|
||||
**נוסחה:**
|
||||
```
|
||||
אחוז_שינוי = (מספר_מילים_שהשתנו / סך_מילים_בטיוטה_המקורית) × 100
|
||||
```
|
||||
|
||||
**מה נספר כ"שינוי":**
|
||||
- **הוספת מילה** = 1 שינוי
|
||||
- **מחיקת מילה** = 1 שינוי
|
||||
- **החלפת מילה** = 1 שינוי
|
||||
- **הוספת סעיף שלם** = מספר המילים בסעיף החדש
|
||||
- **מחיקת סעיף שלם** = מספר המילים בסעיף שנמחק
|
||||
|
||||
**מה לא נספר:**
|
||||
- שינויי פיסוק (נקודה, פסיק) = 0
|
||||
- שינויי רווח/שורה חדשה = 0
|
||||
- שינוי סדר סעיפים (ללא שינוי תוכן) = 0
|
||||
|
||||
**דוגמה:**
|
||||
- טיוטה: 5,000 מילים
|
||||
- דפנה שינתה 400 מילים (200 מחיקות, 100 הוספות, 100 החלפות)
|
||||
- אחוז שינוי: 400/5,000 × 100 = **8%** → עומד ביעד (≤10%)
|
||||
| **זמן לטיוטה** — מרגע העלאת חומרים עד DOCX | יום עבודה | חצי יום | מדידת זמן מקצה לקצה |
|
||||
| **הפניות לא מוולדות** — ציטוטים/מקורות שלא עברו ולידציה מול מסמכים שסופקו | 0% | 0% | ולידציה אוטומטית עם grounding — כל הפניה מקושרת למסמך מקור |
|
||||
| **מענה לכל טענה** — כל טענה בבלוק ז מקבלת מענה בבלוק י | 100% | 100% | בדיקת קישור טענה ← מענה |
|
||||
| **משקלות בלוקים** — בטווח יחסי הזהב ±10% | עומד | עומד | ספירת מילים אוטומטית |
|
||||
| **רקע ניטרלי** — בלוק ו ללא מילות שיפוט או ציטוטי צדדים | עובר ולידציה | עובר ולידציה | סקריפט ולידציה אוטומטי |
|
||||
|
||||
---
|
||||
|
||||
## סעיף 10: סיכונים ומגבלות
|
||||
|
||||
| סיכון | חומרה | הסתברות | מנגנון הגנה |
|
||||
|-------|-------|---------|------------|
|
||||
| **הזיית מקור** — המערכת מצטטת פסק דין שלא קיים | קריטית | בינונית | כלל ברזל: רק מה שסופק. ולידציה אוטומטית של כל הפניה |
|
||||
| **רקע לא ניטרלי** — מילות שיפוט ברקע | גבוהה | בינונית | סקריפט ולידציה + רשימת מילים אסורות |
|
||||
| **טענה ללא מענה** — טענה מבלוק ז שלא נענתה בבלוק י | גבוהה | בינונית | קישור אוטומטי טענה ← מענה + בדיקת כיסוי |
|
||||
| **שאננות אוטומציה** — דפנה מפסיקה לבדוק טיוטות | גבוהה | גבוהה | מדידת אחוז שינוי בלולאת למידה — אם יורד מתחת ל-1% → התראה |
|
||||
| **הטיית אוטומציה** — חיים מאמץ תקדים שגוי | גבוהה | בינונית | הצגת 3-5 חלופות, לא המלצה אחת |
|
||||
| **סגנון לא מתאים** — המערכת כותבת בסגנון חיים ולא בסגנון דפנה | גבוהה | גבוהה בהתחלה | למידה מ-7 החלטות סופיות + לולאת למידה מתמשכת |
|
||||
| **שינוי פסיקה** — תקדים שהמערכת מסתמכת עליו בוטל/שונה | גבוהה | נמוכה | המערכת מסתמכת רק על מה שסופק — לא מחפשת באינטרנט |
|
||||
| **תלות ב-API** — Anthropic API לא זמין | בינונית | נמוכה | אין workaround — המערכת לא עובדת בלי API |
|
||||
| **חוסר בנתוני אימון** — אין החלטות לפיצויים (9xxx) | בינונית | ודאי | צריך להמתין עד שיהיו החלטות מהסוג הזה |
|
||||
| **מורכבות חריגה** — תיק עם 10+ צדדים או 50+ מסמכים | בינונית | נמוכה | אין מגבלה טכנית אבל אין ניסיון — צריך בדיקה |
|
||||
| **חריגת context window** — תיק מורכב עם חומרים רבים שחורגים מ-context window | קריטית | בינונית-גבוהה | מדידת גודל חומרים בטוקנים לפני עיבוד. אסטרטגיית chunking/summarization למסמכים ארוכים. סף התראה כשמתקרבים ל-80% מה-context. סדר עדיפויות: מסמכים קריטיים (ערר, תשובה) לפני נספחים |
|
||||
| **prompt injection ממסמכי מקור** — מסמכים מצדדים חיצוניים יכולים להכיל טקסט שמשפיע על ה-LLM | גבוהה | נמוכה | הפרדה בין הוראות מערכת לתוכן מסמכים. סניטיזציה של קלט. flagging של דפוסים חשודים |
|
||||
|
||||
### מגבלות ידועות
|
||||
|
||||
| מגבלה | השלכה |
|
||||
|-------|-------|
|
||||
| אין החלטות פיצויים (9xxx) לאימון | המערכת לא תוכל לכתוב החלטות פיצויים עד שנלמד מהן |
|
||||
| רק 7 החלטות סופיות לאימון | הסגנון עלול להיות לא מדויק — ישתפר עם כל החלטה שחוזרת מדפנה |
|
||||
| המערכת לא בודקת עדכניות פסיקה | אם תקדים בוטל — המערכת לא תדע. חיים אחראי לבדוק |
|
||||
| המערכת לא מחפשת פסיקה באינטרנט | רק מה שסופק כמסמך — יתרון (אין הזיות) וחיסרון (עלולה לפספס) |
|
||||
@@ -8,7 +8,6 @@ dependencies = [
|
||||
"asyncpg>=0.29.0",
|
||||
"pgvector>=0.3.0",
|
||||
"voyageai>=0.3.0",
|
||||
"anthropic>=0.40.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"pymupdf>=1.25.0",
|
||||
@@ -17,6 +16,7 @@ dependencies = [
|
||||
"redis>=5.0.0",
|
||||
"rq>=1.16.0",
|
||||
"pillow>=10.0.0",
|
||||
"google-cloud-vision>=3.7.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -44,16 +44,24 @@ REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6380/0")
|
||||
|
||||
# Voyage AI
|
||||
VOYAGE_API_KEY = os.environ.get("VOYAGE_API_KEY", "")
|
||||
VOYAGE_MODEL = "voyage-3-large"
|
||||
VOYAGE_MODEL = os.environ.get("VOYAGE_MODEL", "voyage-law-2")
|
||||
VOYAGE_DIMENSIONS = 1024
|
||||
|
||||
# Anthropic (for Claude Vision OCR)
|
||||
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
||||
# Google Cloud Vision (OCR for scanned PDFs)
|
||||
GOOGLE_CLOUD_VISION_API_KEY = os.environ.get("GOOGLE_CLOUD_VISION_API_KEY", "")
|
||||
|
||||
# Data directory
|
||||
DATA_DIR = Path(os.environ.get("DATA_DIR", str(Path.home() / "legal-ai" / "data")))
|
||||
CASES_DIR = DATA_DIR / "cases"
|
||||
TRAINING_DIR = DATA_DIR / "training"
|
||||
EXPORTS_DIR = DATA_DIR / "exports" # legacy exports only
|
||||
|
||||
# Cases directory — flat structure: data/cases/{case_number}/
|
||||
CASES_DIR = DATA_DIR / "cases"
|
||||
|
||||
|
||||
def find_case_dir(case_number: str) -> Path:
|
||||
"""Return the case directory for a given case number."""
|
||||
return CASES_DIR / case_number
|
||||
|
||||
# Chunking parameters
|
||||
CHUNK_SIZE_TOKENS = 600
|
||||
@@ -61,8 +69,8 @@ CHUNK_OVERLAP_TOKENS = 100
|
||||
|
||||
# External service allowlist — case materials may ONLY be sent to these domains
|
||||
ALLOWED_EXTERNAL_SERVICES = {
|
||||
"api.anthropic.com", # Claude API (text generation, OCR)
|
||||
"api.voyageai.com", # Voyage AI (embeddings)
|
||||
"vision.googleapis.com", # Google Cloud Vision (OCR)
|
||||
}
|
||||
|
||||
# Audit
|
||||
|
||||
@@ -217,6 +217,15 @@ async def draft_section(
|
||||
return await drafting.draft_section(case_number, section, instructions)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_chair_directions(case_number: str) -> str:
|
||||
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
|
||||
קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI.
|
||||
מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות.
|
||||
"""
|
||||
return await drafting.get_chair_directions(case_number)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_decision_template(case_number: str) -> str:
|
||||
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""
|
||||
@@ -337,6 +346,29 @@ async def ingest_final_version(
|
||||
return await workflow.ingest_final_version(case_number, file_path, final_text)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def record_chair_feedback(
|
||||
case_number: str,
|
||||
feedback_text: str,
|
||||
block_id: str = "block-yod",
|
||||
category: str = "missing_content",
|
||||
lesson_extracted: str = "",
|
||||
) -> str:
|
||||
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
|
||||
return await workflow.record_chair_feedback(
|
||||
case_number, feedback_text, block_id, category, lesson_extracted,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_chair_feedback(
|
||||
case_number: str = "",
|
||||
category: str = "",
|
||||
unresolved_only: bool = True,
|
||||
) -> str:
|
||||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||||
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
||||
|
||||
|
||||
def main():
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
@@ -18,22 +18,12 @@ import re
|
||||
from datetime import date
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db, embeddings
|
||||
from legal_mcp.services import db, embeddings, claude_session
|
||||
from legal_mcp.services.lessons import get_content_checklist
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
# ── Block configuration ───────────────────────────────────────────
|
||||
|
||||
@@ -226,6 +216,8 @@ BLOCK_PROMPTS = {
|
||||
- **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין)
|
||||
- מספור רציף
|
||||
|
||||
{content_checklist}
|
||||
|
||||
## כיוון מאושר (חובה):
|
||||
{direction_context}
|
||||
|
||||
@@ -321,6 +313,15 @@ async def write_block(
|
||||
outcome = (decision or {}).get("outcome", "rejected")
|
||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||
|
||||
# Content checklist — tells block-yod WHAT topics to cover
|
||||
content_checklist = ""
|
||||
if block_id == "block-yod":
|
||||
content_checklist = get_content_checklist(
|
||||
appeal_type=case.get("appeal_type", ""),
|
||||
subject=case.get("subject", ""),
|
||||
subject_categories=case.get("subject_categories", []),
|
||||
)
|
||||
|
||||
# Format prompt — per Anthropic long-context best practices:
|
||||
# Place source documents FIRST (top of prompt), instructions LAST.
|
||||
# "Queries at the end can improve response quality by up to 30%"
|
||||
@@ -334,6 +335,7 @@ async def write_block(
|
||||
style_context=style_context,
|
||||
discussion_context=discussion_context,
|
||||
structure_guidance=structure_guidance,
|
||||
content_checklist=content_checklist,
|
||||
)
|
||||
|
||||
# Restructure: sources first, then instructions
|
||||
@@ -353,49 +355,10 @@ async def write_block(
|
||||
if not dir_doc.get("approved"):
|
||||
raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.")
|
||||
|
||||
# Call Claude
|
||||
# Call Claude via Claude Code session (no API)
|
||||
model_key = block_cfg["model"]
|
||||
model = MODEL_MAP.get(model_key, MODEL_MAP["sonnet"])
|
||||
temperature = block_cfg["temp"]
|
||||
max_tokens = block_cfg.get("max_tokens", 4096)
|
||||
|
||||
client = _get_anthropic()
|
||||
|
||||
kwargs: dict = {
|
||||
"model": model,
|
||||
"max_tokens": max_tokens,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
|
||||
if model_key == "opus":
|
||||
# Opus 4.6: use adaptive thinking — Claude decides when and how much to think.
|
||||
# Per Anthropic docs: temperature must be 1 when thinking is enabled.
|
||||
# budget_tokens not needed with adaptive thinking.
|
||||
kwargs["temperature"] = 1
|
||||
kwargs["thinking"] = {"type": "enabled", "budget_tokens": max(16000, max_tokens // 2)}
|
||||
else:
|
||||
kwargs["temperature"] = temperature
|
||||
|
||||
# Streaming required when max_tokens > 21,333 (Anthropic requirement)
|
||||
use_stream = max_tokens > 21000 or kwargs.get("thinking")
|
||||
|
||||
if use_stream:
|
||||
content_parts = []
|
||||
with client.messages.stream(**kwargs) as stream:
|
||||
for event in stream:
|
||||
pass # consume stream
|
||||
response = stream.get_final_message()
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
content_parts.append(block.text)
|
||||
content = "\n".join(content_parts)
|
||||
else:
|
||||
message = client.messages.create(**kwargs)
|
||||
content = ""
|
||||
for block in message.content:
|
||||
if block.type == "text":
|
||||
content = block.text
|
||||
break
|
||||
timeout = claude_session.LONG_TIMEOUT if model_key == "opus" else claude_session.DEFAULT_TIMEOUT
|
||||
content = claude_session.query(prompt, timeout=timeout)
|
||||
|
||||
return _build_result(block_id, content, block_cfg)
|
||||
|
||||
@@ -735,7 +698,10 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
||||
|
||||
|
||||
async def save_block_content(case_id: UUID, block_id: str, content: str) -> dict:
|
||||
"""Save block content written by Claude Code (or any external writer)."""
|
||||
"""Save block content written by Claude Code (or any external writer).
|
||||
|
||||
Saves to DB and also writes/updates the draft file on disk.
|
||||
"""
|
||||
if block_id not in BLOCK_CONFIG:
|
||||
raise ValueError(f"Unknown block: {block_id}")
|
||||
|
||||
@@ -749,9 +715,37 @@ async def save_block_content(case_id: UUID, block_id: str, content: str) -> dict
|
||||
result["model_used"] = "claude-code"
|
||||
|
||||
await store_block(UUID(decision["id"]), result)
|
||||
|
||||
# Also write/update the draft file on disk
|
||||
await _update_draft_file(case_id, UUID(decision["id"]))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _update_draft_file(case_id: UUID, decision_id: UUID) -> None:
|
||||
"""Rebuild drafts/decision.md from all blocks in DB."""
|
||||
from pathlib import Path
|
||||
|
||||
case = await db.get_case(case_id)
|
||||
if not case:
|
||||
return
|
||||
|
||||
case_dir = config.find_case_dir(case["case_number"])
|
||||
draft_dir = case_dir / "drafts"
|
||||
draft_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pool = await db.get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"SELECT content FROM decision_blocks WHERE decision_id = $1 AND content != '' ORDER BY block_index",
|
||||
decision_id,
|
||||
)
|
||||
|
||||
draft_path = draft_dir / "decision.md"
|
||||
draft_path.write_text("\n\n".join(row["content"] for row in rows if row["content"]), encoding="utf-8")
|
||||
logger.info("Draft file updated: %s (%d blocks)", draft_path, len(rows))
|
||||
|
||||
|
||||
# ── Renumbering ───────────────────────────────────────────────────
|
||||
|
||||
async def renumber_all_blocks(decision_id: UUID) -> dict:
|
||||
|
||||
@@ -12,23 +12,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.config import parse_llm_json
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import db, claude_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
BRAINSTORM_PROMPT = """אתה יועץ משפטי מומחה בתכנון ובניה. תפקידך לסייע בגיבוש כיוון להחלטת ועדת ערר.
|
||||
|
||||
@@ -145,15 +134,7 @@ async def generate_directions(
|
||||
{doc_context or '(אין מסמכים בתיק)'}
|
||||
"""
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
messages=[{"role": "user", "content": user_content}],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
result = parse_llm_json(raw)
|
||||
result = claude_session.query_json(user_content, timeout=120)
|
||||
if result is None:
|
||||
logger.warning("Failed to parse brainstorm response: %s", raw[:300])
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""חילוץ טענות מכתבי טענות (ערר, תשובה) באמצעות Claude API.
|
||||
"""חילוץ טענות מכתבי טענות (ערר, תשובה) באמצעות Claude Code session.
|
||||
|
||||
שתי גישות:
|
||||
1. extract_claims_with_ai — חילוץ עם Claude (לכתבי טענות קלט)
|
||||
1. extract_claims_with_ai — חילוץ עם Claude Code headless (לכתבי טענות קלט)
|
||||
2. extract_claims_from_block — חילוץ regex (מבלוק ז של החלטות סופיות)
|
||||
"""
|
||||
|
||||
@@ -11,23 +11,12 @@ import logging
|
||||
import re
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.config import parse_llm_json
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import db, claude_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
EXTRACT_CLAIMS_PROMPT = """אתה מנתח מסמכים משפטיים בתחום תכנון ובניה. תפקידך לחלץ טענות מכתב טענות.
|
||||
|
||||
@@ -93,27 +82,15 @@ async def extract_claims_with_ai(
|
||||
chunks = [text]
|
||||
|
||||
all_claims = []
|
||||
client = _get_anthropic()
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk_label = f" (חלק {i+1}/{len(chunks)})" if len(chunks) > 1 else ""
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=8192,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
prompt = (
|
||||
f"{EXTRACT_CLAIMS_PROMPT}\n\n"
|
||||
f"{context}{chunk_label}\n\n"
|
||||
f"--- תחילת מסמך ---\n{chunk}\n--- סוף מסמך ---"
|
||||
),
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
claims = parse_llm_json(raw)
|
||||
claims = claude_session.query_json(prompt, timeout=120)
|
||||
if claims is None:
|
||||
logger.warning("Failed to parse claims for chunk %d: %s", i, raw[:200])
|
||||
continue
|
||||
@@ -137,6 +114,25 @@ async def extract_claims_with_ai(
|
||||
return [c for c in claims if "party_role" in c and "claim_text" in c]
|
||||
|
||||
|
||||
def _infer_claim_type(doc_type: str, source_name: str) -> str:
|
||||
"""Determine claim_type from document type and title.
|
||||
|
||||
- 'claim' = from appeal documents (כתב ערר)
|
||||
- 'response' = from original response documents (כתב תשובה)
|
||||
- 'reply' = from supplementary responses (תגובה, השלמת טיעון)
|
||||
"""
|
||||
name_lower = source_name.lower() if source_name else ""
|
||||
if doc_type == "appeal" or "כתב ערר" in name_lower:
|
||||
return "claim"
|
||||
if "כתב תשובה" in name_lower:
|
||||
return "response"
|
||||
if any(kw in name_lower for kw in ["תגובת", "השלמת טיעון", "תגובה"]):
|
||||
return "reply"
|
||||
if doc_type == "response":
|
||||
return "response"
|
||||
return "claim"
|
||||
|
||||
|
||||
# ── Regex-based extraction (from existing decisions) ──────────────
|
||||
|
||||
PARTY_PATTERNS = [
|
||||
@@ -252,6 +248,11 @@ async def extract_and_store_claims(
|
||||
if not claims:
|
||||
return {"status": "no_claims", "total": 0, "source": source_name}
|
||||
|
||||
# Determine claim_type from document type and title
|
||||
claim_type = _infer_claim_type(doc_type, source_name)
|
||||
for c in claims:
|
||||
c["claim_type"] = claim_type
|
||||
|
||||
stored = await db.store_claims(case_id, claims, source_document=source_name)
|
||||
|
||||
# Summarize by role
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
"""סיווג אוטומטי של מסמכים וזיהוי צדדים.
|
||||
|
||||
שלוש פונקציות:
|
||||
1. classify_document — סיווג סוג מסמך (ערר/תשובה/פרוטוקול/...)
|
||||
2. identify_parties — זיהוי צדדים (עוררים, משיבים, ועדה, מבקשי היתר)
|
||||
3. detect_appeal_type — זיהוי סוג ערר לפי מספר תיק
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.config import parse_llm_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
# ── סיווג סוג מסמך ──────────────────────────────────────────────────
|
||||
|
||||
DOC_TYPES = {
|
||||
"appeal": "כתב ערר",
|
||||
"response": "תשובה / כתב תשובה",
|
||||
"protocol": "פרוטוקול דיון",
|
||||
"plan": "תכנית (תב\"ע)",
|
||||
"permit": "היתר בנייה",
|
||||
"court_decision": "פסק דין / החלטת בית משפט",
|
||||
"decision": "החלטת ועדה",
|
||||
"appraisal": "שומה / חוות דעת שמאית",
|
||||
"objection": "התנגדות",
|
||||
"exhibit": "נספח / מסמך תומך",
|
||||
"reference": "מסמך עזר אחר",
|
||||
}
|
||||
|
||||
CLASSIFY_PROMPT = """אתה מסווג מסמכים משפטיים בתחום תכנון ובניה.
|
||||
|
||||
קרא את תחילת המסמך וסווג אותו לאחד מהסוגים הבאים:
|
||||
- appeal — כתב ערר (מוגש לוועדת ערר)
|
||||
- response — כתב תשובה (תגובת הצד שכנגד או הוועדה המקומית)
|
||||
- protocol — פרוטוקול דיון (רישום מדיון שהתקיים)
|
||||
- plan — תכנית (תב"ע, תכנית מתאר, תכנית מפורטת)
|
||||
- permit — היתר בנייה (או בקשה להיתר)
|
||||
- court_decision — פסק דין או החלטה של בית משפט
|
||||
- decision — החלטת ועדה מקומית או ועדת ערר
|
||||
- appraisal — שומה או חוות דעת שמאית
|
||||
- objection — התנגדות (לתכנית, להיתר)
|
||||
- exhibit — נספח או מסמך תומך
|
||||
- reference — מסמך עזר אחר
|
||||
|
||||
החזר JSON בלבד בפורמט:
|
||||
{"doc_type": "...", "confidence": 0.0-1.0, "reasoning": "הסבר קצר"}
|
||||
"""
|
||||
|
||||
PARTIES_PROMPT = """אתה מנתח מסמכים משפטיים בתחום תכנון ובניה.
|
||||
|
||||
קרא את המסמך וזהה את הצדדים המעורבים. חפש:
|
||||
- עוררים (appellants) — מי שמגיש את הערר
|
||||
- משיבים (respondents) — הצד שכנגד (לרוב ועדה מקומית, או מבקש היתר)
|
||||
- ועדה מקומית (committee) — שם הוועדה המקומית
|
||||
- מבקשי היתר (permit_applicants) — מי שביקש את ההיתר (אם שונה מהעוררים/משיבים)
|
||||
|
||||
החזר JSON בלבד בפורמט:
|
||||
{
|
||||
"appellants": ["שם1", "שם2"],
|
||||
"respondents": ["שם1", "שם2"],
|
||||
"committee": "שם הוועדה המקומית (אם מצוין)",
|
||||
"permit_applicants": ["שם1"],
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
|
||||
אם לא ניתן לזהות צד מסוים, החזר רשימה ריקה. אל תמציא שמות.
|
||||
"""
|
||||
|
||||
|
||||
async def classify_document(text: str) -> dict:
|
||||
"""סיווג סוג מסמך על בסיס הטקסט.
|
||||
|
||||
Args:
|
||||
text: טקסט המסמך (מספיק 3000 תווים ראשונים)
|
||||
|
||||
Returns:
|
||||
dict עם doc_type, confidence, reasoning
|
||||
"""
|
||||
# Use first 3000 chars — usually enough for headers and intro
|
||||
sample = text[:3000]
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=512,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"{CLASSIFY_PROMPT}\n\n--- תחילת מסמך ---\n{sample}\n--- סוף דגימה ---",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
result = parse_llm_json(raw)
|
||||
if result is None:
|
||||
logger.warning("Failed to parse classification response: %s", raw)
|
||||
return {"doc_type": "reference", "confidence": 0.0, "reasoning": "סיווג נכשל"}
|
||||
|
||||
# Validate doc_type
|
||||
if result.get("doc_type") not in DOC_TYPES:
|
||||
result["doc_type"] = "reference"
|
||||
result["confidence"] = 0.0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def identify_parties(text: str) -> dict:
|
||||
"""זיהוי צדדים מתוך טקסט מסמך.
|
||||
|
||||
Args:
|
||||
text: טקסט המסמך (מספיק 5000 תווים ראשונים)
|
||||
|
||||
Returns:
|
||||
dict עם appellants, respondents, committee, permit_applicants, confidence
|
||||
"""
|
||||
# Use first 5000 chars — parties usually in header/intro
|
||||
sample = text[:5000]
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=512,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"{PARTIES_PROMPT}\n\n--- תחילת מסמך ---\n{sample}\n--- סוף דגימה ---",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
result = parse_llm_json(raw)
|
||||
if result is None:
|
||||
logger.warning("Failed to parse parties response: %s", raw)
|
||||
return {
|
||||
"appellants": [],
|
||||
"respondents": [],
|
||||
"committee": "",
|
||||
"permit_applicants": [],
|
||||
"confidence": 0.0,
|
||||
}
|
||||
|
||||
# Normalize structure
|
||||
return {
|
||||
"appellants": result.get("appellants", []),
|
||||
"respondents": result.get("respondents", []),
|
||||
"committee": result.get("committee", ""),
|
||||
"permit_applicants": result.get("permit_applicants", []),
|
||||
"confidence": result.get("confidence", 0.0),
|
||||
}
|
||||
|
||||
|
||||
# ── זיהוי סוג ערר לפי מספר תיק ─────────────────────────────────────
|
||||
|
||||
APPEAL_TYPES = {
|
||||
"licensing": "רישוי ובנייה", # 1xxx
|
||||
"betterment": "היטל השבחה", # 8xxx
|
||||
"compensation": "פיצויים (ס' 197)", # 9xxx
|
||||
}
|
||||
|
||||
|
||||
def detect_appeal_type(case_number: str) -> dict:
|
||||
"""זיהוי סוג ערר לפי מספר תיק.
|
||||
|
||||
Convention:
|
||||
1xxx = רישוי ובנייה
|
||||
8xxx = היטל השבחה
|
||||
9xxx = פיצויים (ס' 197)
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק (e.g. "1078-24", "8042-23", "9015-22")
|
||||
|
||||
Returns:
|
||||
dict עם appeal_type, appeal_type_hebrew, confidence
|
||||
"""
|
||||
# Extract the numeric prefix before any dash/slash
|
||||
match = re.match(r"(\d+)", case_number.strip())
|
||||
if not match:
|
||||
return {
|
||||
"appeal_type": "",
|
||||
"appeal_type_hebrew": "",
|
||||
"confidence": 0.0,
|
||||
}
|
||||
|
||||
num = int(match.group(1))
|
||||
first_digit = str(num)[0] if num > 0 else ""
|
||||
|
||||
if first_digit == "1":
|
||||
appeal_type = "licensing"
|
||||
elif first_digit == "8":
|
||||
appeal_type = "betterment"
|
||||
elif first_digit == "9":
|
||||
appeal_type = "compensation"
|
||||
else:
|
||||
return {
|
||||
"appeal_type": "",
|
||||
"appeal_type_hebrew": "",
|
||||
"confidence": 0.5,
|
||||
}
|
||||
|
||||
return {
|
||||
"appeal_type": appeal_type,
|
||||
"appeal_type_hebrew": APPEAL_TYPES[appeal_type],
|
||||
"confidence": 1.0,
|
||||
}
|
||||
|
||||
|
||||
async def classify_and_identify(text: str, case_number: str = "") -> dict:
|
||||
"""סיווג מלא: סוג מסמך + צדדים + סוג ערר.
|
||||
|
||||
Args:
|
||||
text: טקסט המסמך
|
||||
case_number: מספר תיק (אופציונלי, לזיהוי סוג ערר)
|
||||
|
||||
Returns:
|
||||
dict עם classification, parties, appeal_type
|
||||
"""
|
||||
classification = await classify_document(text)
|
||||
parties = await identify_parties(text)
|
||||
appeal_type = detect_appeal_type(case_number) if case_number else {
|
||||
"appeal_type": "",
|
||||
"appeal_type_hebrew": "",
|
||||
"confidence": 0.0,
|
||||
}
|
||||
|
||||
return {
|
||||
"classification": classification,
|
||||
"parties": parties,
|
||||
"appeal_type": appeal_type,
|
||||
}
|
||||
84
mcp-server/src/legal_mcp/services/claude_session.py
Normal file
84
mcp-server/src/legal_mcp/services/claude_session.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Claude Code session bridge — runs prompts via `claude -p` instead of API.
|
||||
|
||||
All LLM calls in the project should use this module instead of calling
|
||||
the Anthropic API directly. This uses the local Claude Code CLI which
|
||||
runs on the user's claude.ai session — zero API cost.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from legal_mcp.config import parse_llm_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default timeout for claude -p calls (seconds)
|
||||
DEFAULT_TIMEOUT = 120
|
||||
LONG_TIMEOUT = 300 # For complex tasks like block writing
|
||||
|
||||
|
||||
def query(prompt: str, timeout: int = DEFAULT_TIMEOUT, max_turns: int = 1) -> str:
|
||||
"""Send a prompt to Claude Code headless and return the text response.
|
||||
|
||||
Passes the prompt via stdin (not argv) to avoid the OS ARG_MAX limit —
|
||||
prompts can be 500K+ chars when analyzing a full style corpus.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send.
|
||||
timeout: Max seconds to wait.
|
||||
max_turns: Max conversation turns (1 = single response).
|
||||
|
||||
Returns:
|
||||
The text response from Claude.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If claude CLI is not available or fails.
|
||||
"""
|
||||
cmd = [
|
||||
"claude", "-p",
|
||||
"--output-format", "json",
|
||||
"--max-turns", str(max_turns),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError("Claude CLI not found. Install Claude Code or add 'claude' to PATH.")
|
||||
except subprocess.TimeoutExpired:
|
||||
raise RuntimeError(f"Claude CLI timed out after {timeout}s")
|
||||
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.strip()[:500] if result.stderr else "unknown error"
|
||||
raise RuntimeError(f"Claude CLI failed (exit {result.returncode}): {stderr}")
|
||||
|
||||
stdout = result.stdout.strip()
|
||||
if not stdout:
|
||||
raise RuntimeError("Claude CLI returned empty response")
|
||||
|
||||
# claude -p --output-format json returns {"type":"result","result":"..."}
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
if isinstance(data, dict) and "result" in data:
|
||||
return data["result"]
|
||||
return stdout
|
||||
except json.JSONDecodeError:
|
||||
return stdout
|
||||
|
||||
|
||||
def query_json(prompt: str, timeout: int = DEFAULT_TIMEOUT) -> dict | list | None:
|
||||
"""Send a prompt and parse the response as JSON.
|
||||
|
||||
Uses parse_llm_json for robust parsing (handles markdown wrapping, truncation).
|
||||
"""
|
||||
raw = query(prompt, timeout=timeout)
|
||||
return parse_llm_json(raw)
|
||||
@@ -358,6 +358,22 @@ CREATE TABLE IF NOT EXISTS case_law_embeddings (
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- Chair Feedback (הערות דפנה על טיוטות)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chair_feedback (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
case_id UUID REFERENCES cases(id) ON DELETE SET NULL,
|
||||
block_id TEXT DEFAULT '', -- block-yod, block-vav, etc.
|
||||
feedback_text TEXT NOT NULL, -- ההערה של דפנה
|
||||
category TEXT DEFAULT 'other', -- missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||
lesson_extracted TEXT DEFAULT '', -- הלקח שהופק
|
||||
applied_to TEXT[] DEFAULT '{}', -- לאילו קבצים/כללים הלקח יושם
|
||||
resolved BOOLEAN DEFAULT FALSE, -- האם הלקח יושם
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- Indexes
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
@@ -572,14 +588,15 @@ async def store_claims(case_id: UUID, claims: list[dict], source_document: str =
|
||||
)
|
||||
for claim in claims:
|
||||
await conn.execute(
|
||||
"""INSERT INTO claims (case_id, party_role, party_name, claim_text, claim_index, source_document)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
"""INSERT INTO claims (case_id, party_role, party_name, claim_text, claim_index, source_document, claim_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
|
||||
case_id,
|
||||
claim["party_role"],
|
||||
claim.get("party_name", ""),
|
||||
claim["claim_text"],
|
||||
claim.get("claim_index", 0),
|
||||
source_document,
|
||||
claim.get("claim_type", "claim"),
|
||||
)
|
||||
return len(claims)
|
||||
|
||||
@@ -686,6 +703,16 @@ async def update_decision(decision_id: UUID, **fields) -> None:
|
||||
|
||||
# ── Chunks & Vectors ───────────────────────────────────────────────
|
||||
|
||||
async def delete_document_chunks(document_id: UUID) -> int:
|
||||
"""Delete all chunks for a document (used before reprocessing)."""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
result = await conn.execute(
|
||||
"DELETE FROM document_chunks WHERE document_id = $1", document_id
|
||||
)
|
||||
return int(result.split()[-1]) # e.g. "DELETE 5" -> 5
|
||||
|
||||
|
||||
async def store_chunks(
|
||||
document_id: UUID,
|
||||
case_id: UUID | None,
|
||||
@@ -783,6 +810,51 @@ async def add_to_style_corpus(
|
||||
return corpus_id
|
||||
|
||||
|
||||
async def delete_from_style_corpus(corpus_id: UUID) -> dict:
|
||||
"""Remove a decision from style_corpus + related documents (cascades chunks).
|
||||
|
||||
Also tries to delete the [קורפוס] document associated by title match,
|
||||
since the current training pipeline inserts style_corpus with document_id=NULL.
|
||||
"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
row = await conn.fetchrow(
|
||||
"DELETE FROM style_corpus WHERE id = $1 "
|
||||
"RETURNING decision_number, document_id",
|
||||
corpus_id,
|
||||
)
|
||||
if not row:
|
||||
return {"deleted": False, "reason": "not found"}
|
||||
|
||||
docs_deleted = 0
|
||||
if row["document_id"]:
|
||||
await conn.execute(
|
||||
"DELETE FROM documents WHERE id = $1", row["document_id"]
|
||||
)
|
||||
docs_deleted = 1
|
||||
else:
|
||||
# Best-effort: match a [קורפוס] document by the decision_number
|
||||
# in its title. Only for single, unambiguous matches.
|
||||
if row["decision_number"]:
|
||||
docs = await conn.fetch(
|
||||
"SELECT id FROM documents "
|
||||
"WHERE case_id IS NULL AND title LIKE $1",
|
||||
f"%{row['decision_number']}%",
|
||||
)
|
||||
if len(docs) == 1:
|
||||
await conn.execute(
|
||||
"DELETE FROM documents WHERE id = $1", docs[0]["id"]
|
||||
)
|
||||
docs_deleted = 1
|
||||
|
||||
return {
|
||||
"deleted": True,
|
||||
"decision_number": row["decision_number"],
|
||||
"docs_deleted": docs_deleted,
|
||||
}
|
||||
|
||||
|
||||
async def get_style_patterns(pattern_type: str | None = None) -> list[dict]:
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
@@ -930,3 +1002,72 @@ async def search_precedents(
|
||||
|
||||
results.sort(key=lambda x: x["score"], reverse=True)
|
||||
return results[:limit]
|
||||
|
||||
|
||||
# ── Chair feedback ────────────────────────────────────────────────
|
||||
|
||||
async def record_chair_feedback(
|
||||
case_id: UUID | None,
|
||||
block_id: str,
|
||||
feedback_text: str,
|
||||
category: str = "other",
|
||||
lesson_extracted: str = "",
|
||||
) -> UUID:
|
||||
"""Record feedback from the chair (Dafna) on a draft block."""
|
||||
pool = await get_pool()
|
||||
feedback_id = uuid4()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""INSERT INTO chair_feedback
|
||||
(id, case_id, block_id, feedback_text, category, lesson_extracted)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
feedback_id, case_id, block_id, feedback_text, category,
|
||||
lesson_extracted,
|
||||
)
|
||||
return feedback_id
|
||||
|
||||
|
||||
async def list_chair_feedback(
|
||||
case_id: UUID | None = None,
|
||||
category: str | None = None,
|
||||
unresolved_only: bool = False,
|
||||
) -> list[dict]:
|
||||
"""List chair feedback, optionally filtered."""
|
||||
pool = await get_pool()
|
||||
conditions = []
|
||||
params: list = []
|
||||
idx = 1
|
||||
|
||||
if case_id:
|
||||
conditions.append(f"case_id = ${idx}")
|
||||
params.append(case_id)
|
||||
idx += 1
|
||||
if category:
|
||||
conditions.append(f"category = ${idx}")
|
||||
params.append(category)
|
||||
idx += 1
|
||||
if unresolved_only:
|
||||
conditions.append("resolved = FALSE")
|
||||
|
||||
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
f"SELECT * FROM chair_feedback {where} ORDER BY created_at DESC",
|
||||
*params,
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def resolve_chair_feedback(
|
||||
feedback_id: UUID,
|
||||
applied_to: list[str],
|
||||
) -> None:
|
||||
"""Mark feedback as resolved and record where it was applied."""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""UPDATE chair_feedback
|
||||
SET resolved = TRUE, applied_to = $2
|
||||
WHERE id = $1""",
|
||||
feedback_id, applied_to,
|
||||
)
|
||||
|
||||
@@ -169,11 +169,20 @@ async def export_decision(case_id: UUID, output_path: str | None = None) -> str:
|
||||
|
||||
_write_block_to_docx(doc, block_id, block["title"], content)
|
||||
|
||||
# Determine output path
|
||||
# Determine output path — versioned under cases/{case_number}/exports/
|
||||
if not output_path:
|
||||
case_dir = config.CASES_DIR / case["case_number"] / "output"
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = str(case_dir / f"החלטה-{case['case_number']}.docx")
|
||||
export_dir = config.find_case_dir(case["case_number"]) / "exports"
|
||||
export_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Find next version number
|
||||
existing = sorted(export_dir.glob("טיוטה-v*.docx"))
|
||||
next_ver = 1
|
||||
for p in existing:
|
||||
try:
|
||||
ver = int(p.stem.split("-v")[1])
|
||||
next_ver = max(next_ver, ver + 1)
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
output_path = str(export_dir / f"טיוטה-v{next_ver}.docx")
|
||||
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(output_path)
|
||||
|
||||
@@ -1,32 +1,118 @@
|
||||
"""Text extraction from PDF, DOCX, and RTF files.
|
||||
|
||||
Primary PDF extraction: Claude Vision API (for scanned documents).
|
||||
Fallback: PyMuPDF direct text extraction (for born-digital PDFs).
|
||||
Primary PDF extraction: PyMuPDF direct text (for born-digital PDFs).
|
||||
Fallback: Google Cloud Vision OCR (for scanned documents).
|
||||
Post-processing: Hebrew abbreviation quote fixer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import anthropic
|
||||
import fitz # PyMuPDF
|
||||
from docx import Document as DocxDocument
|
||||
from google.cloud import vision
|
||||
from striprtf.striprtf import rtf_to_text
|
||||
|
||||
from legal_mcp import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
# ── Google Cloud Vision client ───────────────────────────────────
|
||||
|
||||
_vision_client: vision.ImageAnnotatorClient | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
def _get_vision_client() -> vision.ImageAnnotatorClient:
|
||||
global _vision_client
|
||||
if _vision_client is None:
|
||||
_vision_client = vision.ImageAnnotatorClient(
|
||||
client_options={"api_key": config.GOOGLE_CLOUD_VISION_API_KEY}
|
||||
)
|
||||
return _vision_client
|
||||
|
||||
|
||||
# ── Hebrew text quality detection ────────────────────────────────
|
||||
|
||||
_HEBREW_RE = re.compile(r'[\u0590-\u05FF]')
|
||||
_WORD_RE = re.compile(r'\S+')
|
||||
|
||||
|
||||
def _text_quality_ok(text: str) -> bool:
|
||||
"""Check if extracted text is real content vs broken OCR layer.
|
||||
|
||||
Returns True if text appears to be genuine Hebrew legal content.
|
||||
Broken OCR layers from scanned PDFs often have:
|
||||
- Very short words / single-character fragments
|
||||
- Each word on its own line (high words-per-line ratio)
|
||||
- Non-Hebrew characters mixed in
|
||||
"""
|
||||
words = _WORD_RE.findall(text)
|
||||
if len(words) < 10:
|
||||
return False
|
||||
|
||||
# Average word length — real Hebrew words avg 4-6 chars.
|
||||
avg_len = sum(len(w) for w in words) / len(words)
|
||||
if avg_len < 2.5:
|
||||
return False
|
||||
|
||||
# Percentage of single-character "words"
|
||||
single_char_pct = sum(1 for w in words if len(w) == 1) / len(words)
|
||||
if single_char_pct > 0.4:
|
||||
return False
|
||||
|
||||
# Words per line — broken OCR puts each word on its own line.
|
||||
# Real text has 5-15 words per line; broken OCR has ~1-2.
|
||||
lines = [l for l in text.split("\n") if l.strip()]
|
||||
if lines:
|
||||
words_per_line = len(words) / len(lines)
|
||||
if words_per_line < 3.0:
|
||||
return False
|
||||
|
||||
# Hebrew character ratio among letter characters
|
||||
letters = re.findall(r'[a-zA-Z\u0590-\u05FF]', text)
|
||||
if letters:
|
||||
hebrew_pct = sum(1 for c in letters if _HEBREW_RE.match(c)) / len(letters)
|
||||
if hebrew_pct < 0.5:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ── Hebrew abbreviation quote fixer ──────────────────────────────
|
||||
|
||||
_HEBREW_ABBREV_FIXES: dict[str, str] = {
|
||||
'עוהייד': 'עוה"ד',
|
||||
'עוייד': 'עו"ד',
|
||||
'הנייל': 'הנ"ל',
|
||||
'מצייב': 'מצ"ב',
|
||||
'ביהמייש': 'ביהמ"ש',
|
||||
'תייז': 'ת"ז',
|
||||
'עייי': 'ע"י',
|
||||
'אחייכ': 'אח"כ',
|
||||
'סייק': 'ס"ק',
|
||||
'דייר': 'ד"ר',
|
||||
'כדוייח': 'כדו"ח',
|
||||
'חווייד': 'חוו"ד',
|
||||
'מייר': 'מ"ר',
|
||||
'יחייד': 'יח"ד',
|
||||
'בייכ': 'ב"כ',
|
||||
}
|
||||
|
||||
_ABBREV_PATTERN = re.compile(
|
||||
'|'.join(re.escape(k) for k in sorted(_HEBREW_ABBREV_FIXES, key=len, reverse=True))
|
||||
)
|
||||
|
||||
|
||||
def _fix_hebrew_quotes(text: str) -> str:
|
||||
"""Fix known Hebrew abbreviation quote replacements from Google Vision OCR."""
|
||||
return _ABBREV_PATTERN.sub(lambda m: _HEBREW_ABBREV_FIXES[m.group()], text)
|
||||
|
||||
|
||||
# ── Extraction ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def extract_text(file_path: str) -> tuple[str, int]:
|
||||
@@ -52,65 +138,53 @@ async def extract_text(file_path: str) -> tuple[str, int]:
|
||||
|
||||
|
||||
async def _extract_pdf(path: Path) -> tuple[str, int]:
|
||||
"""Extract text from PDF. Try direct text first, fall back to Claude Vision for scanned pages."""
|
||||
"""Extract text from PDF.
|
||||
|
||||
Try direct text first, fall back to Google Cloud Vision for scanned
|
||||
or broken-OCR pages.
|
||||
"""
|
||||
doc = fitz.open(str(path))
|
||||
page_count = len(doc)
|
||||
pages_text: list[str] = []
|
||||
|
||||
for page_num in range(page_count):
|
||||
page = doc[page_num]
|
||||
# Try direct text extraction first
|
||||
text = page.get_text().strip()
|
||||
|
||||
if len(text) > 50:
|
||||
# Sufficient text found - born-digital page
|
||||
if len(text) > 50 and _text_quality_ok(text):
|
||||
pages_text.append(text)
|
||||
logger.debug("Page %d: direct text extraction (%d chars)", page_num + 1, len(text))
|
||||
logger.debug("Page %d: direct extraction (%d chars, quality OK)", page_num + 1, len(text))
|
||||
else:
|
||||
# Likely scanned - use Claude Vision
|
||||
logger.info("Page %d: using Claude Vision OCR", page_num + 1)
|
||||
pix = page.get_pixmap(dpi=200)
|
||||
reason = "insufficient text" if len(text) <= 50 else "low quality OCR layer"
|
||||
logger.info("Page %d: Google Vision OCR (%s)", page_num + 1, reason)
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
ocr_text = await _ocr_with_claude(img_bytes, page_num + 1)
|
||||
ocr_text = await asyncio.to_thread(
|
||||
_ocr_with_google_vision, img_bytes, page_num + 1
|
||||
)
|
||||
pages_text.append(ocr_text)
|
||||
|
||||
doc.close()
|
||||
return "\n\n".join(pages_text), page_count
|
||||
|
||||
|
||||
async def _ocr_with_claude(image_bytes: bytes, page_num: int) -> str:
|
||||
"""OCR a single page image using Claude Vision API."""
|
||||
client = _get_anthropic()
|
||||
b64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
def _ocr_with_google_vision(image_bytes: bytes, page_num: int) -> str:
|
||||
"""OCR a single page image using Google Cloud Vision API."""
|
||||
client = _get_vision_client()
|
||||
image = vision.Image(content=image_bytes)
|
||||
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": b64_image,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"חלץ את כל הטקסט מהתמונה הזו. זהו מסמך משפטי בעברית. "
|
||||
"שמור על מבנה הפסקאות המקורי. "
|
||||
"החזר רק את הטקסט המחולץ, ללא הערות נוספות."
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
response = client.document_text_detection(
|
||||
image=image,
|
||||
image_context=vision.ImageContext(language_hints=["he"]),
|
||||
)
|
||||
return message.content[0].text
|
||||
|
||||
if response.error.message:
|
||||
raise RuntimeError(
|
||||
f"Google Vision error on page {page_num}: {response.error.message}"
|
||||
)
|
||||
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
return _fix_hebrew_quotes(text)
|
||||
|
||||
|
||||
def _extract_docx(path: Path) -> str:
|
||||
|
||||
@@ -12,23 +12,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.config import parse_llm_json
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import db, claude_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
def compute_diff_stats(draft_text: str, final_text: str) -> dict:
|
||||
"""חישוב סטטיסטיקות השוואה בין טיוטה לסופית."""
|
||||
@@ -93,25 +82,15 @@ async def analyze_changes(draft_text: str, final_text: str) -> dict:
|
||||
draft_sample = draft_text[:max_chars]
|
||||
final_sample = final_text[:max_chars]
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": f"""{LESSONS_PROMPT}
|
||||
prompt = f"""{LESSONS_PROMPT}
|
||||
|
||||
--- טיוטה ---
|
||||
{draft_sample}
|
||||
|
||||
--- גרסה סופית ---
|
||||
{final_sample}
|
||||
""",
|
||||
}],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
result = parse_llm_json(raw)
|
||||
"""
|
||||
result = claude_session.query_json(prompt, timeout=120)
|
||||
if result is None:
|
||||
logger.warning("Failed to parse lessons response")
|
||||
return {"changes": [], "new_expressions": [], "overall_assessment": raw[:200]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Lessons learned from comparing AI drafts to Dafna Tamir's final decisions.
|
||||
|
||||
Source: /data/uploads/לקחים-לעדכון-שרת-כתיבת-החלטות.md
|
||||
Source: docs/legal-decision-lessons.md
|
||||
Based on analysis of: Hecht 1180-1181 (rejection) and Beit HaKerem 1126/25+1141/25 (partial acceptance).
|
||||
"""
|
||||
|
||||
@@ -329,3 +329,193 @@ def format_ratios_comment(outcome: str, section: str) -> str:
|
||||
lo, hi = ratios[section]
|
||||
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
||||
return ""
|
||||
|
||||
|
||||
# ── Content checklists by appeal subtype ──────────────────────────
|
||||
# Based on systematic analysis of 24 decisions from Dafna's corpus.
|
||||
# See: docs/corpus-analysis.md
|
||||
|
||||
CONTENT_CHECKLISTS: dict[str, str] = {
|
||||
"licensing_substantive": """## צ'קליסט תוכן — ערר רישוי מהותי (חובה)
|
||||
הדיון חייב לכלול את הנושאים הרלוונטיים מהרשימה הבאה.
|
||||
**אל תדלג על נושא שרלוונטי לתיק — בדוק כל סעיף.**
|
||||
|
||||
### א. הקשר תכנוני רחב (חובה בכל ערר מהותי)
|
||||
- תכניות חלות — ציין את התכניות הרלוונטיות ברמה מקומית, מחוזית וארצית (לפי הצורך)
|
||||
- ייעוד הקרקע — מה הייעוד בתכנית? מה השימושים המותרים?
|
||||
- אופי הסביבה — מרקם בנוי, צפיפות, אופי שכונה/ישוב
|
||||
- *דוגמה*: בערר פרומר — 12 סעיפים על MI/200, תמ"א 35, תמ"מ 30/1
|
||||
|
||||
### ב. ניתוח הוראות תכנית (כשיש שאלה של התאמה/סטייה)
|
||||
- ציטוט ישיר מהוראות התכנית הרלוונטיות (200-600 מילים לכל ציטוט)
|
||||
- פרשנות — מה תכלית ההוראה?
|
||||
- יישום — האם הבקשה תואמת או סוטה?
|
||||
- *דוגמה*: בערר לבנון — ניתוח חתכים של נספח בינוי מול הבקשה
|
||||
|
||||
### ג. חניה (כשרלוונטי — מופיע ב-8 מתוך 24 החלטות)
|
||||
- הוראות תכנית + נספח תנועה (ציטוט ישיר)
|
||||
- חישוב מקומות חניה נדרשים vs. מסופקים
|
||||
- חלופות: קרן חניה, חפיפת שימושים, קרבה לתח"צ
|
||||
- *דוגמה*: בערר בית הכרם — 8 סעיפים, 400+ מילים מהוראות תכנית 5166ב
|
||||
|
||||
### ד. קווי בניין ומרווחים (כשרלוונטי)
|
||||
- הוראת תכנית על מרווחים
|
||||
- סטייה ניכרת? — תקנה 2(19) / הלכת בן-יקר-גת
|
||||
- הצדקה + מידתיות — פגיעה בשכנים?
|
||||
|
||||
### ה. גובה וקומות (כשרלוונטי)
|
||||
- הוראת תכנית + נספח בינוי (חתכים)
|
||||
- מטרת ההגבלה — למה יש הגבלת גובה כאן?
|
||||
- סטייה ניכרת — תקנה 2(10) / 2(8)
|
||||
|
||||
### ו. פגיעה בשכנים (כשרלוונטי)
|
||||
- ממצאי סיור באתר
|
||||
- השפעה: צל, פרטיות, רעש, נוף
|
||||
- מידתיות — האם הפגיעה סבירה?
|
||||
|
||||
### ז. שימוש חורג (כשרלוונטי)
|
||||
- מה השימוש המותר בתכנית? מה השימוש המבוקש?
|
||||
- "מבחן ההתאמה" — האם השימוש מתאים למיקום?
|
||||
- תנאים ומגבלות
|
||||
""",
|
||||
|
||||
"licensing_threshold": """## צ'קליסט תוכן — ערר רישוי סף/סמכות
|
||||
הערר עוסק בשאלות סף — אין צורך בדיון תכנוני מקיף.
|
||||
|
||||
### א. שאלת הסמכות
|
||||
- סעיפי חוק רלוונטיים (ס' 12ב, 152, וכו')
|
||||
- פסיקה על גבולות הסמכות
|
||||
|
||||
### ב. זכות ערר
|
||||
- מי רשאי לערור? באיזה מסלול?
|
||||
- הלכת שפר (עע"מ 317/10) — כשרלוונטית
|
||||
|
||||
### ג. שיהוי (אם רלוונטי)
|
||||
""",
|
||||
|
||||
"licensing_property": """## צ'קליסט תוכן — ערר רישוי קנייני
|
||||
הערר עוסק בעיקר בשאלת תימוכין קנייניים — דיון משפטי.
|
||||
|
||||
### א. מסגרת נורמטיבית
|
||||
- הלכת עייזן, בני אליעזר, רוזן — "היתכנות קניינית"
|
||||
- ס' 71ב לחוק המקרקעין
|
||||
|
||||
### ב. בחינת הראיות
|
||||
- הסכמות, רישום, היסטוריית בנייה
|
||||
- חלוקה דה-פקטו ארוכת שנים
|
||||
|
||||
### ג. הפרדה בין קניין לתכנון
|
||||
- גוף תכנוני אינו מכריע בסכסוכי קניין
|
||||
- "היתכנות קניינית" ≠ הוכחת בעלות
|
||||
|
||||
### ד. שאלות תכנוניות (אם רלוונטיות)
|
||||
- אם הערר עולה גם שאלות תכנוניות — דון בהן בנפרד
|
||||
""",
|
||||
|
||||
"tama38": """## צ'קליסט תוכן — ערר תמ"א 38
|
||||
הדיון חייב לאזן בין אינטרס ציבורי לפגיעה בשכנים.
|
||||
|
||||
### א. אינטרס ציבורי — חיזוק/התחדשות
|
||||
- עוצמת האינטרס — בניין גדול vs. בית בודד
|
||||
- "בית בודד" מחליש את אינטרס החיזוק
|
||||
- תרומה לרקמה העירונית
|
||||
|
||||
### ב. תכנית אב / מדיניות אזורית
|
||||
- האם יש תכנית אב? מדיניות 16000?
|
||||
- התאמה לראיה כללית vs. אד-הוק
|
||||
|
||||
### ג. ניתוח השוואתי
|
||||
- זכויות לפי תכנית קיימת vs. מבוקש לפי תמ"א 38
|
||||
- שטחים, קומות, קווי בניין — טבלת השוואה
|
||||
|
||||
### ד. שימור (כשרלוונטי)
|
||||
- חוות דעת אגף שימור
|
||||
- השפעה על מיקום/צורת הבניין
|
||||
|
||||
### ה. חניה (כמעט תמיד רלוונטי)
|
||||
- הוראות תכנית + ס' 17 לתמ"א 38
|
||||
- פטורים — קרבה לתח"צ, קרן חניה, תכנית אב
|
||||
- ניתוח מפורט של חלופות
|
||||
|
||||
### ו. פגיעה בשכנים
|
||||
- ממצאי סיור
|
||||
- צל, פרטיות, קרבה
|
||||
- מידתיות — מה הפגיעה ביחס לתועלת?
|
||||
|
||||
### ז. מטרדי בנייה
|
||||
- "מטרד בנייה אינו עילה לסירוב" — אך תנאים נדרשים
|
||||
- תכנית ארגון אתר
|
||||
""",
|
||||
|
||||
"betterment_levy": """## צ'קליסט תוכן — ערר היטל השבחה
|
||||
⚠️ שים לב: אין עדיין החלטות היטל השבחה בקורפוס האימון.
|
||||
הצ'קליסט הזה מבוסס על ידע כללי — לא על ניתוח ספציפי של סגנון דפנה.
|
||||
|
||||
### א. המסגרת הנורמטיבית
|
||||
- התוספת השלישית לחוק התכנון והבנייה
|
||||
- אירוע מס — מה יצר את ההשבחה?
|
||||
|
||||
### ב. שומה
|
||||
- שיטת השומה (שומה מכרעת / שמאי מייעץ)
|
||||
- מועד הקובע
|
||||
- זכויות בנייה — לפני ואחרי
|
||||
|
||||
### ג. שאלות משפטיות
|
||||
- פטורים (ס' 19)
|
||||
- מועדי תשלום
|
||||
- שיערוך
|
||||
|
||||
### ד. ניתוח שמאי
|
||||
- האם השומה תקינה?
|
||||
- פערים בין השומות
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
def get_content_checklist(
|
||||
appeal_type: str = "",
|
||||
subject: str = "",
|
||||
subject_categories: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Return the appropriate content checklist based on case characteristics.
|
||||
|
||||
Determines the subtype from case metadata:
|
||||
- TAMA 38 cases → tama38 checklist
|
||||
- Betterment levy (8xxx) → betterment_levy checklist
|
||||
- Property-only cases → licensing_property checklist
|
||||
- Threshold/jurisdiction cases → licensing_threshold checklist
|
||||
- All other licensing → licensing_substantive checklist
|
||||
"""
|
||||
cats = subject_categories or []
|
||||
subject_lower = subject.lower() if subject else ""
|
||||
appeal_lower = appeal_type.lower() if appeal_type else ""
|
||||
|
||||
# TAMA 38
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["תמ\"א 38", "תמא 38", "תמ\"א38", "חיזוק", "tama"]
|
||||
) or "תמ\"א 38" in cats:
|
||||
return CONTENT_CHECKLISTS["tama38"]
|
||||
|
||||
# Betterment levy
|
||||
if "היטל השבחה" in appeal_lower or "betterment" in appeal_lower or any(
|
||||
"היטל" in c for c in cats
|
||||
):
|
||||
return CONTENT_CHECKLISTS["betterment_levy"]
|
||||
|
||||
# Property-focused (תימוכין קנייניים)
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["תימוכין", "קנייני", "בעלות", "הסכמת דיירים"]
|
||||
):
|
||||
return CONTENT_CHECKLISTS["licensing_property"]
|
||||
|
||||
# Threshold/jurisdiction
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["סמכות", "סף", "סילוק על הסף", "זכות ערר"]
|
||||
):
|
||||
return CONTENT_CHECKLISTS["licensing_threshold"]
|
||||
|
||||
# Default: substantive licensing
|
||||
return CONTENT_CHECKLISTS["licensing_substantive"]
|
||||
|
||||
105
mcp-server/src/legal_mcp/services/local_classifier.py
Normal file
105
mcp-server/src/legal_mcp/services/local_classifier.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Local document classifier — rule-based, no API calls.
|
||||
|
||||
Classifies legal documents by filename patterns and content keywords.
|
||||
Falls back to Claude Code headless (`claude -p`) for ambiguous cases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Filename patterns (checked in order, first match wins) ────────
|
||||
|
||||
_FILENAME_RULES: list[tuple[str, str, float]] = [
|
||||
# (regex pattern on filename, doc_type, confidence)
|
||||
(r"כתב.ערר|כתב-ערר", "appeal", 1.0),
|
||||
(r"תשובה|תשובת|תגובה|תגובת|השלמת.טיעון|בקשה.להשלמת|הודעת.עמדה", "response", 1.0),
|
||||
(r"פרוטוקול", "protocol", 1.0),
|
||||
(r"החלטת?.ביניים|החלטה.לתיקון", "decision", 0.95),
|
||||
(r"הוראות.תכנית|תכנית", "plan", 1.0),
|
||||
(r"היתר", "permit", 1.0),
|
||||
(r"שומה|חוו.ת.דעת", "appraisal", 1.0),
|
||||
(r"התנגדות", "objection", 1.0),
|
||||
# Court decisions: case number patterns
|
||||
(r"(?:עעם|עע.?מ|עתמ|עת.?מ|בג.?צ|בבנ|עא|ע.?א|רעא|רע.?א|עעמ|עתמ)", "court_decision", 1.0),
|
||||
# ערר + number that's NOT part of our case files (i.e. precedent references)
|
||||
(r"^ערר.?\d", "court_decision", 0.9),
|
||||
]
|
||||
|
||||
# ── Content patterns (first 500 chars) ───────────────────────────
|
||||
|
||||
_CONTENT_RULES: list[tuple[str, str, float]] = [
|
||||
(r"בפני\s+ועדת\s+הערר|לפנינו\s+ערר|ניתנה?\s+היום", "decision", 0.85),
|
||||
(r"כתב\s+ערר|העורר.{0,20}מגיש", "appeal", 0.85),
|
||||
(r"כתב\s+תשובה|המשיב.{0,20}משיב", "response", 0.85),
|
||||
(r"פרוטוקול\s+(?:דיון|ישיבה|ועדה)", "protocol", 0.9),
|
||||
(r"בית\s+(?:ה)?משפט|פסק\s+דין|השופט", "court_decision", 0.85),
|
||||
(r"הוראות\s+(?:ה)?תכנית|תב.עה|ייעוד\s+הקרקע", "plan", 0.8),
|
||||
]
|
||||
|
||||
|
||||
def classify(filename: str, text: str = "") -> tuple[str, float]:
|
||||
"""Classify a legal document by filename and content.
|
||||
|
||||
Returns (doc_type, confidence). Confidence > 0.8 means high certainty.
|
||||
"""
|
||||
name = Path(filename).stem
|
||||
|
||||
# Try filename rules
|
||||
for pattern, doc_type, confidence in _FILENAME_RULES:
|
||||
if re.search(pattern, name):
|
||||
logger.info("Local classifier: '%s' → %s (filename, %.2f)", name, doc_type, confidence)
|
||||
return doc_type, confidence
|
||||
|
||||
# Try content rules (first 500 chars)
|
||||
snippet = text[:500] if text else ""
|
||||
for pattern, doc_type, confidence in _CONTENT_RULES:
|
||||
if re.search(pattern, snippet):
|
||||
logger.info("Local classifier: '%s' → %s (content, %.2f)", name, doc_type, confidence)
|
||||
return doc_type, confidence
|
||||
|
||||
logger.info("Local classifier: '%s' → reference (no match, 0.3)", name)
|
||||
return "reference", 0.3
|
||||
|
||||
|
||||
def classify_with_claude_code(filename: str, text: str) -> tuple[str, float]:
|
||||
"""Fallback: use Claude Code headless to classify ambiguous documents.
|
||||
|
||||
Only works when `claude` CLI is available (not in Docker).
|
||||
"""
|
||||
prompt = (
|
||||
"סווג את המסמך המשפטי הבא לאחת הקטגוריות הבאות בלבד:\n"
|
||||
"appeal, response, protocol, decision, plan, permit, appraisal, "
|
||||
"court_decision, exhibit, objection, reference\n\n"
|
||||
f"שם הקובץ: {filename}\n"
|
||||
f"תחילת המסמך:\n{text[:500]}\n\n"
|
||||
'החזר JSON בלבד: {"doc_type": "...", "confidence": 0.9}'
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "-p", prompt, "--output-format", "json", "--max-turns", "1"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
data = json.loads(result.stdout)
|
||||
# claude -p --output-format json wraps in {"result": "..."}
|
||||
inner = data.get("result", data)
|
||||
if isinstance(inner, str):
|
||||
inner = json.loads(inner)
|
||||
doc_type = inner.get("doc_type", "reference")
|
||||
confidence = float(inner.get("confidence", 0.7))
|
||||
logger.info("Claude Code classifier: '%s' → %s (%.2f)", filename, doc_type, confidence)
|
||||
return doc_type, confidence
|
||||
except FileNotFoundError:
|
||||
logger.debug("Claude CLI not available — skipping headless fallback")
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as e:
|
||||
logger.warning("Claude Code classifier failed: %s", e)
|
||||
|
||||
return "reference", 0.3
|
||||
@@ -3,9 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import chunker, classifier, db, embeddings, extractor, references_extractor
|
||||
from legal_mcp.services import chunker, db, embeddings, extractor, references_extractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,37 +38,35 @@ async def process_document(document_id: UUID, case_id: UUID) -> dict:
|
||||
page_count=page_count,
|
||||
)
|
||||
|
||||
# Step 1.5: Classify document and identify parties
|
||||
logger.info("Classifying document")
|
||||
case_number = ""
|
||||
if case_id:
|
||||
case = await db.get_case(case_id)
|
||||
if case:
|
||||
case_number = case.get("case_number", "")
|
||||
classification_result = await classifier.classify_and_identify(text, case_number)
|
||||
await db.update_document(
|
||||
document_id,
|
||||
metadata=classification_result,
|
||||
)
|
||||
logger.info(
|
||||
"Classification: %s (confidence: %.2f), parties found: %d appellants, %d respondents",
|
||||
classification_result["classification"].get("doc_type", "?"),
|
||||
classification_result["classification"].get("confidence", 0),
|
||||
len(classification_result["parties"].get("appellants", [])),
|
||||
len(classification_result["parties"].get("respondents", [])),
|
||||
)
|
||||
# Save extracted text to documents/extracted/ directory
|
||||
original_path = Path(doc["file_path"])
|
||||
extracted_dir = original_path.parent.parent / "extracted"
|
||||
extracted_dir.mkdir(parents=True, exist_ok=True)
|
||||
txt_path = extracted_dir / (original_path.stem + ".txt")
|
||||
try:
|
||||
txt_path.write_text(text, encoding="utf-8")
|
||||
logger.info("Saved extracted text to %s", txt_path)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to save text file (non-fatal): %s", e)
|
||||
|
||||
# Step 1.6: Update case parties if empty
|
||||
if case_id and case:
|
||||
parties = classification_result.get("parties", {})
|
||||
updates = {}
|
||||
if not case.get("appellants") and parties.get("appellants"):
|
||||
updates["appellants"] = parties["appellants"]
|
||||
if not case.get("respondents") and parties.get("respondents"):
|
||||
updates["respondents"] = parties["respondents"]
|
||||
if updates:
|
||||
await db.update_case(case_id, **updates)
|
||||
logger.info("Updated case parties: %s", updates)
|
||||
# Step 1.5: Classify document — local rules first, Claude Code headless fallback
|
||||
classification_result = {}
|
||||
try:
|
||||
from legal_mcp.services import local_classifier
|
||||
filename = Path(doc["file_path"]).name
|
||||
doc_type, confidence = local_classifier.classify(filename, text)
|
||||
if confidence < 0.8:
|
||||
doc_type, confidence = local_classifier.classify_with_claude_code(filename, text)
|
||||
|
||||
# Update doc_type if we got a good classification and current type is generic
|
||||
if confidence >= 0.5 and doc.get("doc_type") in ("reference", "auto"):
|
||||
await db.update_document(document_id, doc_type=doc_type)
|
||||
logger.info("Auto-classified: %s → %s (confidence %.2f)", filename, doc_type, confidence)
|
||||
|
||||
classification_result = {"classification": {"doc_type": doc_type, "confidence": confidence}}
|
||||
await db.update_document(document_id, metadata=classification_result)
|
||||
except Exception as e:
|
||||
logger.warning("Classification failed (non-fatal): %s", e)
|
||||
|
||||
# Step 2: Chunk
|
||||
logger.info("Chunking document (%d chars)", len(text))
|
||||
@@ -96,7 +95,9 @@ async def process_document(document_id: UUID, case_id: UUID) -> dict:
|
||||
|
||||
stored = await db.store_chunks(document_id, case_id, chunk_dicts)
|
||||
|
||||
# Step 5: Extract references (plans, case law, legislation)
|
||||
# Step 5: Extract references (plans, case law, legislation) — non-fatal
|
||||
refs_result = {"plans": 0, "case_law": 0, "case_law_linked": 0, "legislation": 0}
|
||||
try:
|
||||
logger.info("Extracting legal references")
|
||||
refs_result = await references_extractor.extract_and_link_references(
|
||||
document_id, case_id, text,
|
||||
@@ -106,6 +107,8 @@ async def process_document(document_id: UUID, case_id: UUID) -> dict:
|
||||
refs_result["plans"], refs_result["case_law"],
|
||||
refs_result["case_law_linked"], refs_result["legislation"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Reference extraction failed (non-fatal): %s", e)
|
||||
|
||||
await db.update_document(document_id, extraction_status="completed")
|
||||
|
||||
|
||||
404
mcp-server/src/legal_mcp/services/proofreader.py
Normal file
404
mcp-server/src/legal_mcp/services/proofreader.py
Normal file
@@ -0,0 +1,404 @@
|
||||
"""Nevo proofreading service for training corpus.
|
||||
|
||||
Strips Nevo editorial additions (front matter, back matter, page headers,
|
||||
watermarks, inline watermark codes) from legal decision DOCX/PDF/MD files.
|
||||
|
||||
Also extracts metadata (decision number, date, subject categories) via
|
||||
heuristics on cleaned text.
|
||||
|
||||
Used by:
|
||||
* CLI script: scripts/proofread_training_corpus.py
|
||||
* Web API: /api/training/analyze
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
from datetime import date as date_type
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import fitz
|
||||
from docx import Document
|
||||
from google.cloud import vision
|
||||
|
||||
from legal_mcp import config
|
||||
|
||||
# ── Nevo pattern detection ────────────────────────────────────────
|
||||
|
||||
NEVO_PREAMBLE_HEADERS = (
|
||||
"ספרות:",
|
||||
"חקיקה שאוזכרה:",
|
||||
"מיני-רציו:",
|
||||
)
|
||||
|
||||
DECISION_OPENING = re.compile(
|
||||
r"^(עניינו\s|ענייננו\s|עסקינן\s|בפנינו\s|לפנינו\s|בערר\s+שלפנינו|זהו\s+ערר)"
|
||||
)
|
||||
|
||||
DECISION_SECTION_HEADERS = {
|
||||
"רקע",
|
||||
"פתח דבר",
|
||||
"תמצית טענות הצדדים",
|
||||
"העובדות",
|
||||
"הרקע העובדתי",
|
||||
"מבוא",
|
||||
}
|
||||
|
||||
NEVO_POSTAMBLE_MARKERS = (
|
||||
"5129371512937154678313",
|
||||
"בעניין עריכה ושינויים במסמכי פסיקה",
|
||||
"נוסח מסמך זה כפוף לשינויי ניסוח ועריכה",
|
||||
)
|
||||
|
||||
NEVO_INLINE_CODE_RE = re.compile(r"^0?(5129371|54678313)\d*")
|
||||
|
||||
PDF_PAGE_HEADER_RE = re.compile(
|
||||
r"\s*עמוד\s*\n?\s*\d+\s*\n?\s*(?:מתוך|בן)\s*\n?\s*\d+\s*"
|
||||
)
|
||||
PDF_PAGE_ORPHAN_RE = re.compile(r"(?m)^עמוד[^\n]{0,12}$")
|
||||
PDF_PAGE_NUM_LINE_RE = re.compile(r"(?m)^\s*עמוד\s*\n?\s*\d+[·.*]?\s*$")
|
||||
NEVO_URL_RE = re.compile(
|
||||
r"(nevo\.co\.il|neto\.co\.il|netocoal|neetocoal|nevocoal|nevo\.co|rawo\.co\.il)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_FOOTER_JUNK_RE = re.compile(
|
||||
r"^("
|
||||
r"\s*|"
|
||||
r"[-·*.\"\'׳״]+|"
|
||||
r"\d{1,3}[\s\-·*.\"\'׳״]*|"
|
||||
r"עמוד[\s\d\-·*.\"\'׳״]*|"
|
||||
r"[-·*\s\"\'׳״]*[a-zA-Z][a-zA-Z0-9 .\-·*_]{0,30}"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Hebrew abbreviation quote fixes — Google Vision renders ״ as 'יי'
|
||||
_HEBREW_ABBREV_FIXES: dict[str, str] = {
|
||||
"עוהייד": 'עוה"ד', "עוייד": 'עו"ד', "הנייל": 'הנ"ל', "מצייב": 'מצ"ב',
|
||||
"ביהמייש": 'ביהמ"ש', "תייז": 'ת"ז', "עייי": 'ע"י', "אחייכ": 'אח"כ',
|
||||
"סייק": 'ס"ק', "דייר": 'ד"ר', "חווייד": 'חוו"ד', "מייר": 'מ"ר',
|
||||
"יחייד": 'יח"ד', "בייכ": 'ב"כ', "בייה": 'ב"ה', "שייח": 'ש"ח',
|
||||
"יוייר": 'יו"ר', "בליימ": 'בל"מ', "תבייע": 'תב"ע', "תמייא": 'תמ"א',
|
||||
"סייה": 'ס"ה', "שייפ": 'ש"פ', "שצייפ": 'שצ"פ', "שבייצ": 'שב"צ',
|
||||
"עסיים": 'עס"ם', "הייה": 'ה"ה', "פסייד": 'פס"ד', "תיידא": 'תיד"א',
|
||||
"בגייץ": 'בג"ץ', "עתיים": 'עת"ם', "עעיים": 'עע"ם',
|
||||
"כייא": 'כ"א', "כייב": 'כ"ב', "כייג": 'כ"ג', "כייד": 'כ"ד',
|
||||
"כייה": 'כ"ה', "כייו": 'כ"ו', "כייז": 'כ"ז', "כייח": 'כ"ח', "כייט": 'כ"ט',
|
||||
"לייא": 'ל"א',
|
||||
"יייא": 'י"א', "יייב": 'י"ב', "יייג": 'י"ג', "יייד": 'י"ד',
|
||||
"טייו": 'ט"ו', "טייז": 'ט"ז', "יייז": 'י"ז', "יייח": 'י"ח', "יייט": 'י"ט',
|
||||
"תשפייא": 'תשפ"א', "תשפייב": 'תשפ"ב', "תשפייג": 'תשפ"ג',
|
||||
"תשפייד": 'תשפ"ד', "תשפייה": 'תשפ"ה', "תשפייו": 'תשפ"ו',
|
||||
"תשפיין": 'תשפ"ן',
|
||||
}
|
||||
_ABBREV_PATTERN = re.compile(
|
||||
"|".join(re.escape(k) for k in sorted(_HEBREW_ABBREV_FIXES, key=len, reverse=True))
|
||||
)
|
||||
|
||||
|
||||
def _fix_hebrew_quotes(text: str) -> str:
|
||||
return _ABBREV_PATTERN.sub(lambda m: _HEBREW_ABBREV_FIXES[m.group()], text)
|
||||
|
||||
|
||||
# ── Google Vision OCR ────────────────────────────────────────────
|
||||
|
||||
_vision_client: vision.ImageAnnotatorClient | None = None
|
||||
|
||||
|
||||
def _get_vision_client() -> vision.ImageAnnotatorClient:
|
||||
global _vision_client
|
||||
if _vision_client is None:
|
||||
if not config.GOOGLE_CLOUD_VISION_API_KEY:
|
||||
raise RuntimeError("GOOGLE_CLOUD_VISION_API_KEY not set")
|
||||
_vision_client = vision.ImageAnnotatorClient(
|
||||
client_options={"api_key": config.GOOGLE_CLOUD_VISION_API_KEY}
|
||||
)
|
||||
return _vision_client
|
||||
|
||||
|
||||
def _ocr_page_image(image_bytes: bytes, page_num: int) -> str:
|
||||
client = _get_vision_client()
|
||||
image = vision.Image(content=image_bytes)
|
||||
response = client.document_text_detection(
|
||||
image=image,
|
||||
image_context=vision.ImageContext(language_hints=["he"]),
|
||||
)
|
||||
if response.error.message:
|
||||
raise RuntimeError(f"Vision error page {page_num}: {response.error.message}")
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
return _fix_hebrew_quotes(text)
|
||||
|
||||
|
||||
# ── DOCX proofreading ────────────────────────────────────────────
|
||||
|
||||
|
||||
def _find_decision_start(paragraphs: list[str]) -> int:
|
||||
"""Find first real decision paragraph, skipping Nevo preamble."""
|
||||
has_nevo_preamble = any(
|
||||
any(p.startswith(h) for h in NEVO_PREAMBLE_HEADERS) for p in paragraphs[:10]
|
||||
)
|
||||
if not has_nevo_preamble:
|
||||
return 0
|
||||
|
||||
for i, p in enumerate(paragraphs):
|
||||
stripped = p.strip()
|
||||
if stripped in DECISION_SECTION_HEADERS:
|
||||
return i
|
||||
if DECISION_OPENING.match(stripped):
|
||||
return i
|
||||
|
||||
for i, p in enumerate(paragraphs):
|
||||
if "קבעה כלהלן" in p or "קבעה את הדברים הבאים" in p:
|
||||
for j in range(i + 1, min(i + 15, len(paragraphs))):
|
||||
if len(paragraphs[j]) > 80 and not paragraphs[j].strip().startswith("*"):
|
||||
return j
|
||||
break
|
||||
|
||||
return min(10, len(paragraphs) - 1)
|
||||
|
||||
|
||||
def _find_decision_end(paragraphs: list[str]) -> int:
|
||||
"""First paragraph that is a Nevo postamble marker (exclusive end)."""
|
||||
for i, p in enumerate(paragraphs):
|
||||
for marker in NEVO_POSTAMBLE_MARKERS:
|
||||
if marker in p:
|
||||
return i
|
||||
return len(paragraphs)
|
||||
|
||||
|
||||
def _strip_inline_nevo_codes(paragraphs: list[str]) -> list[str]:
|
||||
out: list[str] = []
|
||||
for p in paragraphs:
|
||||
stripped = NEVO_INLINE_CODE_RE.sub("", p).strip()
|
||||
if stripped:
|
||||
out.append(stripped)
|
||||
return out
|
||||
|
||||
|
||||
def proofread_docx(path: Path) -> tuple[str, dict]:
|
||||
"""Extract clean decision text from Nevo DOCX. Returns (markdown, stats)."""
|
||||
doc = Document(str(path))
|
||||
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
|
||||
|
||||
start = _find_decision_start(paragraphs)
|
||||
end = _find_decision_end(paragraphs)
|
||||
|
||||
clean = _strip_inline_nevo_codes(paragraphs[start:end])
|
||||
md = "\n\n".join(clean)
|
||||
|
||||
return md, {
|
||||
"source_type": "docx",
|
||||
"total_paragraphs": len(paragraphs),
|
||||
"preamble_stripped": start,
|
||||
"postamble_stripped": len(paragraphs) - end,
|
||||
"clean_paragraphs": len(clean),
|
||||
}
|
||||
|
||||
|
||||
# ── PDF proofreading ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _clean_page_text(text: str) -> str:
|
||||
text = PDF_PAGE_HEADER_RE.sub("\n", text)
|
||||
|
||||
lines = text.split("\n")
|
||||
while lines and _FOOTER_JUNK_RE.match(lines[-1].strip()):
|
||||
lines.pop()
|
||||
text = "\n".join(lines)
|
||||
|
||||
text = NEVO_URL_RE.sub("", text)
|
||||
text = PDF_PAGE_NUM_LINE_RE.sub("", text)
|
||||
text = PDF_PAGE_ORPHAN_RE.sub("", text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def proofread_pdf(path: Path) -> tuple[str, dict]:
|
||||
"""Extract clean decision text from Nevo PDF via Google Vision OCR."""
|
||||
doc = fitz.open(str(path))
|
||||
pages: list[str] = []
|
||||
for i, page in enumerate(doc):
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
text = await asyncio.to_thread(_ocr_page_image, img_bytes, i + 1)
|
||||
pages.append(_clean_page_text(text))
|
||||
await asyncio.sleep(0.1)
|
||||
doc.close()
|
||||
|
||||
body = "\n\n".join(p for p in pages if p)
|
||||
body = re.sub(r"\n{3,}", "\n\n", body)
|
||||
body = re.sub(r"[ \t]+\n", "\n", body)
|
||||
|
||||
for marker in NEVO_POSTAMBLE_MARKERS:
|
||||
idx = body.find(marker)
|
||||
if idx != -1:
|
||||
body = body[:idx].rstrip()
|
||||
break
|
||||
|
||||
return body, {
|
||||
"source_type": "pdf",
|
||||
"pages": len(pages),
|
||||
"chars": len(body),
|
||||
}
|
||||
|
||||
|
||||
# ── MD/TXT passthrough ───────────────────────────────────────────
|
||||
|
||||
|
||||
def proofread_md(path: Path) -> tuple[str, dict]:
|
||||
"""Plain text passthrough for already-clean .md/.txt files."""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
return text, {"source_type": "md", "chars": len(text)}
|
||||
|
||||
|
||||
async def proofread(path: Path) -> tuple[str, dict]:
|
||||
"""Proofread a file based on its extension. Returns (clean_text, stats)."""
|
||||
suffix = path.suffix.lower()
|
||||
if suffix == ".docx":
|
||||
return proofread_docx(path)
|
||||
if suffix == ".pdf":
|
||||
return await proofread_pdf(path)
|
||||
if suffix in (".md", ".txt"):
|
||||
return proofread_md(path)
|
||||
raise ValueError(f"Unsupported file type: {suffix}")
|
||||
|
||||
|
||||
# ── Metadata extraction ──────────────────────────────────────────
|
||||
|
||||
FILENAME_NUMBER_PATTERNS = [
|
||||
re.compile(r"^ARAR-(\d{2})-(\d{3,4})"),
|
||||
re.compile(r"^ערר\s+(\d{3,4})-(\d{2})"),
|
||||
re.compile(r"^ערר\s+(\d{3,4})\s*-"),
|
||||
]
|
||||
LEGACY_MULTI_PATTERN = re.compile(r"(\d{3,4})\+(\d{3,4})")
|
||||
|
||||
|
||||
def decision_number_from_filename(stem: str) -> str | None:
|
||||
"""Extract NUMBER/YY from a filename stem."""
|
||||
m = FILENAME_NUMBER_PATTERNS[0].match(stem)
|
||||
if m:
|
||||
return f"{m.group(2)}/{m.group(1)}"
|
||||
m = FILENAME_NUMBER_PATTERNS[1].match(stem)
|
||||
if m:
|
||||
return f"{m.group(1)}/{m.group(2)}"
|
||||
m = FILENAME_NUMBER_PATTERNS[2].match(stem)
|
||||
if m:
|
||||
return f"{m.group(1)}/??"
|
||||
m = LEGACY_MULTI_PATTERN.search(stem)
|
||||
if m:
|
||||
return f"{m.group(1)}+{m.group(2)}/??"
|
||||
return None
|
||||
|
||||
|
||||
HEBREW_MONTHS = {
|
||||
"ינואר": 1, "בינואר": 1, "פברואר": 2, "בפברואר": 2,
|
||||
"מרץ": 3, "מרס": 3, "במרץ": 3, "במרס": 3,
|
||||
"אפריל": 4, "באפריל": 4, "מאי": 5, "במאי": 5,
|
||||
"יוני": 6, "ביוני": 6, "יולי": 7, "ביולי": 7,
|
||||
"אוגוסט": 8, "באוגוסט": 8, "ספטמבר": 9, "בספטמבר": 9,
|
||||
"אוקטובר": 10, "באוקטובר": 10, "נובמבר": 11, "בנובמבר": 11,
|
||||
"דצמבר": 12, "בדצמבר": 12,
|
||||
}
|
||||
DATE_RE = re.compile(
|
||||
r"(\d{1,2})\s+(ב?(?:ינואר|פברואר|מרץ|מרס|אפריל|מאי|יוני|יולי|אוגוסט|ספטמבר|אוקטובר|נובמבר|דצמבר))\s*[,.]?\s*(\d{4})"
|
||||
)
|
||||
NITNA_RE = re.compile(r"ניתנ[הו]?\s+(?:פה\s+אחד|בדעת\s+רוב|היום)?")
|
||||
|
||||
|
||||
def decision_date_from_text(text: str) -> str | None:
|
||||
tail = text[-2500:] if len(text) > 2500 else text
|
||||
nitna_match = NITNA_RE.search(tail)
|
||||
search_text = tail[nitna_match.start():] if nitna_match else tail
|
||||
m = DATE_RE.search(search_text)
|
||||
if not m:
|
||||
m = DATE_RE.search(tail)
|
||||
if not m:
|
||||
return None
|
||||
day = int(m.group(1))
|
||||
month = HEBREW_MONTHS.get(m.group(2))
|
||||
year = int(m.group(3))
|
||||
if not month:
|
||||
return None
|
||||
try:
|
||||
return date_type(year, month, day).isoformat()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def finalize_decision_number(number: str | None, date_iso: str | None) -> str:
|
||||
if not number:
|
||||
return f"??/{date_iso[2:4]}" if date_iso else ""
|
||||
if number.endswith("/??"):
|
||||
return number.replace("/??", f"/{date_iso[2:4]}") if date_iso else number.replace("/??", "")
|
||||
return number
|
||||
|
||||
|
||||
def categorize(text: str) -> list[str]:
|
||||
"""Heuristic subject category detection based on opening + repetition."""
|
||||
opening = text[:2000]
|
||||
t = text
|
||||
|
||||
cats: list[str] = []
|
||||
|
||||
if re.search(r'תמ[״"\']?א\s*38|תמא\s*38', t):
|
||||
cats.append('תמ"א 38')
|
||||
|
||||
if len(re.findall(r"היטל(?:י)?\s+השבחה", t)) >= 3 or re.search(r"היטל(?:י)?\s+השבחה", opening):
|
||||
cats.append("היטל השבחה")
|
||||
|
||||
p197_re = r"פיצויים\s+לפי\s+(?:ס(?:עיף|')\s*)?197|סעיף\s*197|ס['\"]?\s*197"
|
||||
if len(re.findall(p197_re, t)) >= 2 or re.search(p197_re, opening):
|
||||
cats.append("פיצויים 197")
|
||||
|
||||
if t.count("שימוש חורג") >= 3 or "שימוש חורג" in opening:
|
||||
cats.append("שימוש חורג")
|
||||
|
||||
if len(re.findall(r"\bהקלה\b|\bהקלות\b", t)) >= 3 and re.search(r"\bהקלה\b|\bהקלות\b", opening):
|
||||
cats.append("הקלה")
|
||||
|
||||
if re.search(r"איחוד\s+וחלוקה|חלוקה\s+חדשה|תכנית\s+לחלוקה", t):
|
||||
cats.append("חלוקה")
|
||||
|
||||
if re.search(
|
||||
r"הפקדת\s+ה?תכנית|אישור\s+ה?תכנית|המלצה\s+להפקיד|"
|
||||
r"להפקיד\s+את\s+ה?תכנית|לדון\s+בתכנית|דנה\s+בתכנית|"
|
||||
r"החלטה\s+לאשר\s+ה?תכנית",
|
||||
opening,
|
||||
):
|
||||
cats.append("תכנית")
|
||||
|
||||
if re.search(r"בקשה\s+להיתר|היתר\s+בני(?:י)?ה", opening):
|
||||
cats.append("היתר")
|
||||
|
||||
has_permit_subject = "היתר" in cats or "הקלה" in cats or 'תמ"א 38' in cats
|
||||
if has_permit_subject and "בנייה" not in cats:
|
||||
cats.append("בנייה")
|
||||
|
||||
return cats or ["בנייה"]
|
||||
|
||||
|
||||
async def analyze_file(path: Path) -> dict[str, Any]:
|
||||
"""Proofread a file and extract metadata for review.
|
||||
|
||||
Returns a dict suitable for UI preview with: clean text, metadata,
|
||||
stats, and a short text preview for visual verification.
|
||||
"""
|
||||
clean_text, stats = await proofread(path)
|
||||
num_raw = decision_number_from_filename(path.stem)
|
||||
d_iso = decision_date_from_text(clean_text)
|
||||
number = finalize_decision_number(num_raw, d_iso)
|
||||
cats = categorize(clean_text)
|
||||
|
||||
return {
|
||||
"filename": path.name,
|
||||
"clean_text": clean_text,
|
||||
"preview": clean_text[:500],
|
||||
"decision_number": number,
|
||||
"decision_date": d_iso or "",
|
||||
"subject_categories": cats,
|
||||
"stats": stats,
|
||||
"chars": len(clean_text),
|
||||
}
|
||||
@@ -18,11 +18,9 @@ import logging
|
||||
import re
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.config import parse_llm_json
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import db, claude_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -89,14 +87,6 @@ def check_neutral_background(blocks: list[dict]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
_anthropic_client: anthropic.Anthropic | None = None
|
||||
|
||||
|
||||
def _get_anthropic() -> anthropic.Anthropic:
|
||||
global _anthropic_client
|
||||
if _anthropic_client is None:
|
||||
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
return _anthropic_client
|
||||
|
||||
|
||||
CLAIMS_CHECK_PROMPT = """אתה בודק איכות החלטות משפטיות. קיבלת רשימת טענות שהועלו בכתבי הטענות, ואת בלוק הדיון של ההחלטה.
|
||||
@@ -146,24 +136,15 @@ async def check_claims_coverage(blocks: list[dict], claims: list[dict]) -> dict:
|
||||
# Send full discussion — don't truncate
|
||||
discussion = yod["content"]
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=8192,
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": f"""{CLAIMS_CHECK_PROMPT}
|
||||
prompt = f"""{CLAIMS_CHECK_PROMPT}
|
||||
|
||||
## טענות ({len(source_claims)}):
|
||||
{claims_text}
|
||||
|
||||
## בלוק הדיון:
|
||||
{discussion}""",
|
||||
}],
|
||||
)
|
||||
{discussion}"""
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
parsed = parse_llm_json(raw)
|
||||
parsed = claude_session.query_json(prompt, timeout=120)
|
||||
if parsed is None:
|
||||
logger.warning("Failed to parse claims check: %s", raw[:300])
|
||||
# Fallback: assume all covered (don't block export on parse failure)
|
||||
|
||||
436
mcp-server/src/legal_mcp/services/research_md.py
Normal file
436
mcp-server/src/legal_mcp/services/research_md.py
Normal file
@@ -0,0 +1,436 @@
|
||||
"""Parser for analysis-and-research.md produced by the legal-analyst agent.
|
||||
|
||||
Extracts the structured content (threshold claims, issues, sections) into
|
||||
a JSON-serializable dict for UI rendering, and supports atomic in-place
|
||||
updates of the "עמדת ועדת הערר" (chair position) field in each subsection.
|
||||
|
||||
The parser is intentionally tolerant: the file format is under active
|
||||
development, so we extract what we find rather than enforcing a strict
|
||||
schema. Missing sections return empty/None values.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Placeholder strings — any of these means "not yet filled"
|
||||
CHAIR_POSITION_PLACEHOLDERS = (
|
||||
"[ימולא ע\"י יו\"ר הוועדה]",
|
||||
"[ימולא ע'י יו'ר הוועדה]",
|
||||
"[ימולא על ידי יו\"ר הוועדה]",
|
||||
"[לא מולא]",
|
||||
"[טרם מולא]",
|
||||
)
|
||||
|
||||
CHAIR_POSITION_LABEL = "עמדת ועדת הערר"
|
||||
|
||||
# Matches "## N. title" or "## title" for main sections
|
||||
MAIN_SECTION_RE = re.compile(r"^##\s+(\d+)\.?\s+(.+?)$", re.MULTILINE)
|
||||
|
||||
# Matches "### title" for subsections (threshold claims, issues)
|
||||
SUBSECTION_RE = re.compile(r"^###\s+(.+?)$", re.MULTILINE)
|
||||
|
||||
# Matches "**LABEL:**" field markers — handles both inline and block variants:
|
||||
# "**עמדת המבקשת:** Some text on same line"
|
||||
# "**שאלות משפטיות:**\n1. First question"
|
||||
# The label itself must not contain ** or newlines.
|
||||
FIELD_LABEL_RE = re.compile(r"^\*\*([^\n*]+?):\*\*[ \t]*", re.MULTILINE)
|
||||
|
||||
# Matches the case number in the H1
|
||||
CASE_NUMBER_RE = re.compile(r"#\s*ניתוח.*?ערר\s+([\d/\-]+)", re.MULTILINE)
|
||||
|
||||
# Matches the date line
|
||||
DATE_RE = re.compile(r"^תאריך:\s*(.+?)\s*$", re.MULTILINE)
|
||||
|
||||
|
||||
def _is_placeholder(text: str) -> bool:
|
||||
"""Check if a field value is one of the placeholder strings (empty)."""
|
||||
stripped = text.strip()
|
||||
if not stripped:
|
||||
return True
|
||||
for ph in CHAIR_POSITION_PLACEHOLDERS:
|
||||
if ph in stripped:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_chair_position(text: str) -> str:
|
||||
"""Return empty string for placeholders, otherwise the text."""
|
||||
if _is_placeholder(text):
|
||||
return ""
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _split_main_sections(content: str) -> list[tuple[str, str, str]]:
|
||||
"""Split content into (number, title, body) tuples for each H2 section.
|
||||
|
||||
Handles both numbered (## 1. title) and unnumbered (## title) H2s.
|
||||
Body is everything up to the next H2.
|
||||
"""
|
||||
# Find all H2 positions
|
||||
h2_positions = []
|
||||
for m in re.finditer(r"^##\s+(.+?)$", content, re.MULTILINE):
|
||||
title = m.group(1).strip()
|
||||
num_match = re.match(r"^(\d+)\.?\s+(.+)", title)
|
||||
if num_match:
|
||||
number = num_match.group(1)
|
||||
title = num_match.group(2).strip()
|
||||
else:
|
||||
number = ""
|
||||
h2_positions.append((m.start(), m.end(), number, title))
|
||||
|
||||
sections = []
|
||||
for i, (_start, end, number, title) in enumerate(h2_positions):
|
||||
next_start = h2_positions[i + 1][0] if i + 1 < len(h2_positions) else len(content)
|
||||
body = content[end:next_start].strip()
|
||||
sections.append((number, title, body))
|
||||
return sections
|
||||
|
||||
|
||||
def _split_subsections(body: str) -> list[tuple[str, str]]:
|
||||
"""Split a section body by H3 subsections.
|
||||
|
||||
Returns list of (title, content) — content is everything until next H3.
|
||||
Leading text before first H3 is discarded at this level.
|
||||
"""
|
||||
h3_positions = []
|
||||
for m in re.finditer(r"^###\s+(.+?)$", body, re.MULTILINE):
|
||||
h3_positions.append((m.start(), m.end(), m.group(1).strip()))
|
||||
|
||||
if not h3_positions:
|
||||
return []
|
||||
|
||||
subs = []
|
||||
for i, (_start, end, title) in enumerate(h3_positions):
|
||||
next_start = h3_positions[i + 1][0] if i + 1 < len(h3_positions) else len(body)
|
||||
content = body[end:next_start].strip()
|
||||
# Strip trailing horizontal rule "---"
|
||||
content = re.sub(r"\s*---\s*$", "", content).strip()
|
||||
subs.append((title, content))
|
||||
return subs
|
||||
|
||||
|
||||
def _extract_fields(text: str) -> list[dict]:
|
||||
"""Extract bold-label fields from a subsection body.
|
||||
|
||||
Returns list of {"label": str, "content": str} in document order.
|
||||
A field runs from its "**LABEL:**" marker until the next one (or EOS).
|
||||
"""
|
||||
matches = list(FIELD_LABEL_RE.finditer(text))
|
||||
if not matches:
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for i, m in enumerate(matches):
|
||||
label = m.group(1).strip()
|
||||
content_start = m.end()
|
||||
content_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
content = text[content_start:content_end].strip()
|
||||
# Strip trailing horizontal rule
|
||||
content = re.sub(r"\s*---\s*$", "", content).strip()
|
||||
fields.append({"label": label, "content": content})
|
||||
return fields
|
||||
|
||||
|
||||
def _build_subsection_dict(
|
||||
title: str, body: str, id_prefix: str, number: int
|
||||
) -> dict:
|
||||
"""Build a structured dict for a threshold claim or issue subsection.
|
||||
|
||||
- id: stable identifier used by update endpoint (e.g. 'threshold_1')
|
||||
- title: the H3 title
|
||||
- number: 1-based ordinal
|
||||
- fields: ordered list of {label, content} pairs
|
||||
- chair_position: extracted separately for UI editing (normalized empty)
|
||||
"""
|
||||
fields = _extract_fields(body)
|
||||
|
||||
# Split title at ": " for cleaner display
|
||||
display_title = title
|
||||
if ": " in title:
|
||||
parts = title.split(": ", 1)
|
||||
display_title = parts[1] if len(parts) > 1 else title
|
||||
|
||||
chair_position = ""
|
||||
regular_fields = []
|
||||
for f in fields:
|
||||
if f["label"] == CHAIR_POSITION_LABEL:
|
||||
chair_position = _normalize_chair_position(f["content"])
|
||||
else:
|
||||
regular_fields.append(f)
|
||||
|
||||
return {
|
||||
"id": f"{id_prefix}_{number}",
|
||||
"number": number,
|
||||
"title": display_title,
|
||||
"raw_title": title,
|
||||
"fields": regular_fields,
|
||||
"chair_position": chair_position,
|
||||
}
|
||||
|
||||
|
||||
def parse(file_path: Path) -> dict[str, Any]:
|
||||
"""Parse analysis-and-research.md into a structured dict.
|
||||
|
||||
Returns a dict with header info, plain-text sections, threshold_claims[],
|
||||
issues[], and conclusions. Tolerant to missing sections.
|
||||
"""
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
|
||||
# Header info from H1 and date line
|
||||
case_match = CASE_NUMBER_RE.search(content)
|
||||
case_number = case_match.group(1) if case_match else ""
|
||||
date_match = DATE_RE.search(content)
|
||||
date_str = date_match.group(1) if date_match else ""
|
||||
|
||||
stat = file_path.stat()
|
||||
mtime_iso = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"header": {
|
||||
"case_number": case_number,
|
||||
"date": date_str,
|
||||
"file_path": str(file_path),
|
||||
"file_size": stat.st_size,
|
||||
"modified_at": mtime_iso,
|
||||
},
|
||||
"represented_party": "",
|
||||
"procedural_background": "",
|
||||
"agreed_facts": "",
|
||||
"disputed_facts": "",
|
||||
"threshold_claims": [],
|
||||
"issues": [],
|
||||
"conclusions": "",
|
||||
"other_sections": [],
|
||||
}
|
||||
|
||||
sections = _split_main_sections(content)
|
||||
|
||||
for number, title, body in sections:
|
||||
title_norm = title.strip()
|
||||
|
||||
if "צד מיוצג" in title_norm:
|
||||
result["represented_party"] = body
|
||||
elif "רקע דיוני" in title_norm:
|
||||
result["procedural_background"] = body
|
||||
elif "עובדות מוסכמות" in title_norm:
|
||||
result["agreed_facts"] = body
|
||||
elif "עובדות שנויות במחלוקת" in title_norm or "שנויות" in title_norm:
|
||||
result["disputed_facts"] = body
|
||||
elif "טענות סף" in title_norm or "טענות הסף" in title_norm:
|
||||
subs = _split_subsections(body)
|
||||
for i, (sub_title, sub_body) in enumerate(subs, start=1):
|
||||
result["threshold_claims"].append(
|
||||
_build_subsection_dict(sub_title, sub_body, "threshold", i)
|
||||
)
|
||||
elif "סוגיות להכרעה" in title_norm or "סוגיות" in title_norm:
|
||||
subs = _split_subsections(body)
|
||||
for i, (sub_title, sub_body) in enumerate(subs, start=1):
|
||||
result["issues"].append(
|
||||
_build_subsection_dict(sub_title, sub_body, "issue", i)
|
||||
)
|
||||
elif "מסקנות" in title_norm or "סיכום" in title_norm:
|
||||
result["conclusions"] = body
|
||||
else:
|
||||
# Unknown section — keep as-is for display
|
||||
result["other_sections"].append(
|
||||
{"number": number, "title": title_norm, "body": body}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ── Chair position in-place update ───────────────────────────────
|
||||
|
||||
|
||||
def _find_subsection_by_id(
|
||||
content: str, section_id: str
|
||||
) -> tuple[int, int, str] | None:
|
||||
"""Locate a subsection's body range in the raw content.
|
||||
|
||||
Given section_id like 'threshold_2' or 'issue_3', walks the file
|
||||
structure and returns (body_start, body_end, body_text) for that
|
||||
subsection. Returns None if not found.
|
||||
"""
|
||||
parts = section_id.split("_")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
kind, idx_str = parts
|
||||
try:
|
||||
target_idx = int(idx_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if kind == "threshold":
|
||||
main_keywords = ("טענות סף", "טענות הסף")
|
||||
elif kind == "issue":
|
||||
main_keywords = ("סוגיות להכרעה", "סוגיות")
|
||||
else:
|
||||
return None
|
||||
|
||||
# Find the main section that contains threshold claims or issues
|
||||
sections_iter = list(re.finditer(r"^##\s+(.+?)$", content, re.MULTILINE))
|
||||
for i, m in enumerate(sections_iter):
|
||||
title = m.group(1).strip()
|
||||
if not any(kw in title for kw in main_keywords):
|
||||
continue
|
||||
|
||||
body_start = m.end()
|
||||
body_end = (
|
||||
sections_iter[i + 1].start() if i + 1 < len(sections_iter) else len(content)
|
||||
)
|
||||
section_body = content[body_start:body_end]
|
||||
|
||||
# Find H3 subsections within
|
||||
h3s = list(re.finditer(r"^###\s+.+?$", section_body, re.MULTILINE))
|
||||
if target_idx < 1 or target_idx > len(h3s):
|
||||
return None
|
||||
|
||||
sub_start_rel = h3s[target_idx - 1].end()
|
||||
sub_end_rel = (
|
||||
h3s[target_idx].start() if target_idx < len(h3s) else len(section_body)
|
||||
)
|
||||
|
||||
abs_start = body_start + sub_start_rel
|
||||
abs_end = body_start + sub_end_rel
|
||||
return abs_start, abs_end, content[abs_start:abs_end]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def update_chair_position(
|
||||
file_path: Path, section_id: str, new_text: str
|
||||
) -> dict[str, Any]:
|
||||
"""Atomically update the chair_position field of one subsection.
|
||||
|
||||
Writes to a temporary file then renames into place (atomic on Linux).
|
||||
Returns {"saved": bool, "section_id": ..., "preview": ...}.
|
||||
Raises FileNotFoundError or ValueError on error.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(str(file_path))
|
||||
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
found = _find_subsection_by_id(content, section_id)
|
||||
if not found:
|
||||
raise ValueError(f"section {section_id} not found")
|
||||
|
||||
_abs_start, _abs_end, subsection_body = found
|
||||
|
||||
# Find the "**עמדת ועדת הערר:**" label within this subsection
|
||||
label_pattern = re.compile(
|
||||
r"(\*\*" + re.escape(CHAIR_POSITION_LABEL) + r":\*\*)\s*\n?([^*]*?)(?=\n\*\*|\n##|\n---|\Z)",
|
||||
re.DOTALL,
|
||||
)
|
||||
m = label_pattern.search(subsection_body)
|
||||
if not m:
|
||||
# Label not present — append it at the end of the subsection
|
||||
# (just before the trailing --- if any)
|
||||
new_block = f"\n\n**{CHAIR_POSITION_LABEL}:**\n{new_text.strip()}\n"
|
||||
new_subsection = subsection_body.rstrip() + new_block
|
||||
new_content = content[:_abs_start] + new_subsection + content[_abs_end:]
|
||||
else:
|
||||
# Replace the existing content of the chair_position field
|
||||
replacement = f"{m.group(1)}\n{new_text.strip() if new_text.strip() else CHAIR_POSITION_PLACEHOLDERS[0]}\n"
|
||||
new_subsection = (
|
||||
subsection_body[: m.start()] + replacement + subsection_body[m.end():]
|
||||
)
|
||||
new_content = content[:_abs_start] + new_subsection + content[_abs_end:]
|
||||
|
||||
# Atomic write
|
||||
tmp_path = file_path.with_suffix(file_path.suffix + ".tmp")
|
||||
tmp_path.write_text(new_content, encoding="utf-8")
|
||||
os.replace(tmp_path, file_path)
|
||||
|
||||
preview = new_text.strip()[:120]
|
||||
return {
|
||||
"saved": True,
|
||||
"section_id": section_id,
|
||||
"preview": preview,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ── Chair directions extraction (for downstream agents) ─────────
|
||||
|
||||
|
||||
def extract_chair_directions(file_path: Path) -> dict[str, Any]:
|
||||
"""Extract only the chair positions from analysis-and-research.md.
|
||||
|
||||
Returns a compact dict that the legal-writer agent can use as direction:
|
||||
|
||||
{
|
||||
"case_number": "1033-25",
|
||||
"file_path": "...",
|
||||
"file_exists": True,
|
||||
"total_items": 9,
|
||||
"filled_count": 3,
|
||||
"empty_count": 6,
|
||||
"status": "partial", # "empty" | "partial" | "complete"
|
||||
"threshold_claims": [
|
||||
{"id": "threshold_1", "number": 1, "title": "...", "direction": "..."},
|
||||
...
|
||||
],
|
||||
"issues": [
|
||||
{"id": "issue_1", "number": 1, "title": "...", "direction": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Used by legal-writer to convert chair positions into direction docs
|
||||
before generating blocks of the decision.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
return {
|
||||
"file_exists": False,
|
||||
"status": "missing",
|
||||
"error": "analysis-and-research.md not found",
|
||||
"threshold_claims": [],
|
||||
"issues": [],
|
||||
"total_items": 0,
|
||||
"filled_count": 0,
|
||||
"empty_count": 0,
|
||||
}
|
||||
|
||||
parsed = parse(file_path)
|
||||
|
||||
def reduce_item(item: dict) -> dict:
|
||||
return {
|
||||
"id": item["id"],
|
||||
"number": item["number"],
|
||||
"title": item["title"],
|
||||
"direction": item.get("chair_position", "") or "",
|
||||
}
|
||||
|
||||
threshold = [reduce_item(t) for t in parsed.get("threshold_claims", [])]
|
||||
issues = [reduce_item(i) for i in parsed.get("issues", [])]
|
||||
|
||||
all_items = threshold + issues
|
||||
total = len(all_items)
|
||||
filled = sum(1 for x in all_items if x["direction"].strip())
|
||||
empty = total - filled
|
||||
|
||||
if total == 0:
|
||||
status = "missing"
|
||||
elif filled == 0:
|
||||
status = "empty"
|
||||
elif filled == total:
|
||||
status = "complete"
|
||||
else:
|
||||
status = "partial"
|
||||
|
||||
return {
|
||||
"file_exists": True,
|
||||
"file_path": str(file_path),
|
||||
"case_number": parsed.get("header", {}).get("case_number", ""),
|
||||
"status": status,
|
||||
"total_items": total,
|
||||
"filled_count": filled,
|
||||
"empty_count": empty,
|
||||
"threshold_claims": threshold,
|
||||
"issues": issues,
|
||||
}
|
||||
@@ -6,10 +6,8 @@ import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import db, claude_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -150,24 +148,16 @@ async def _analyze_single_pass(rows) -> dict:
|
||||
decisions_text += f"\n\n--- החלטה {row['decision_number'] or 'ללא מספר'} ---\n"
|
||||
decisions_text += row["full_text"]
|
||||
|
||||
client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
message = client.messages.create(
|
||||
model="claude-opus-4-6",
|
||||
max_tokens=16384,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": ANALYSIS_PROMPT.format(decisions=decisions_text),
|
||||
}
|
||||
],
|
||||
raw = claude_session.query(
|
||||
ANALYSIS_PROMPT.format(decisions=decisions_text),
|
||||
timeout=claude_session.LONG_TIMEOUT,
|
||||
)
|
||||
|
||||
return await _parse_and_store_patterns(message.content[0].text, len(rows))
|
||||
return await _parse_and_store_patterns(raw, len(rows))
|
||||
|
||||
|
||||
async def _analyze_multi_pass(rows) -> dict:
|
||||
"""Analyze each decision individually, then synthesize patterns."""
|
||||
client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
all_patterns = []
|
||||
|
||||
# Pass 1: Analyze each decision individually
|
||||
@@ -175,18 +165,12 @@ async def _analyze_multi_pass(rows) -> dict:
|
||||
decision_text = f"--- החלטה {row['decision_number'] or 'ללא מספר'} ---\n"
|
||||
decision_text += row["full_text"]
|
||||
|
||||
message = client.messages.create(
|
||||
model="claude-opus-4-6",
|
||||
max_tokens=8192,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": SINGLE_DECISION_PROMPT.format(decision=decision_text),
|
||||
}
|
||||
],
|
||||
raw = claude_session.query(
|
||||
SINGLE_DECISION_PROMPT.format(decision=decision_text),
|
||||
timeout=claude_session.LONG_TIMEOUT,
|
||||
)
|
||||
|
||||
patterns = _extract_json(message.content[0].text)
|
||||
patterns = _extract_json(raw)
|
||||
if patterns:
|
||||
all_patterns.extend(patterns)
|
||||
|
||||
@@ -194,21 +178,15 @@ async def _analyze_multi_pass(rows) -> dict:
|
||||
return {"error": "לא הצלחתי לחלץ דפוסים מההחלטות"}
|
||||
|
||||
# Pass 2: Synthesize across all decisions
|
||||
message = client.messages.create(
|
||||
model="claude-opus-4-6",
|
||||
max_tokens=16384,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": SYNTHESIS_PROMPT.format(
|
||||
raw = claude_session.query(
|
||||
SYNTHESIS_PROMPT.format(
|
||||
num_decisions=len(rows),
|
||||
patterns=json.dumps(all_patterns, ensure_ascii=False, indent=2),
|
||||
),
|
||||
}
|
||||
],
|
||||
timeout=claude_session.LONG_TIMEOUT,
|
||||
)
|
||||
|
||||
return await _parse_and_store_patterns(message.content[0].text, len(rows))
|
||||
return await _parse_and_store_patterns(raw, len(rows))
|
||||
|
||||
|
||||
def _extract_json(response_text: str) -> list | None:
|
||||
|
||||
@@ -60,9 +60,14 @@ async def case_create(
|
||||
)
|
||||
|
||||
# Initialize git repo for the case
|
||||
case_dir = config.CASES_DIR / case_number
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
(case_dir / "documents").mkdir(exist_ok=True)
|
||||
docs_dir = case_dir / "documents"
|
||||
docs_dir.mkdir(exist_ok=True)
|
||||
(docs_dir / "original").mkdir(exist_ok=True)
|
||||
(docs_dir / "extracted").mkdir(exist_ok=True)
|
||||
(docs_dir / "proofread").mkdir(exist_ok=True)
|
||||
(docs_dir / "backup").mkdir(exist_ok=True)
|
||||
(case_dir / "drafts").mkdir(exist_ok=True)
|
||||
|
||||
# Save case metadata
|
||||
@@ -167,7 +172,7 @@ async def case_update(
|
||||
updated = await db.update_case(UUID(case["id"]), **fields)
|
||||
|
||||
# Git commit the update
|
||||
case_dir = config.CASES_DIR / case_number
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
if case_dir.exists():
|
||||
case_json = case_dir / "case.json"
|
||||
case_json.write_text(json.dumps(updated, default=str, ensure_ascii=False, indent=2))
|
||||
|
||||
@@ -39,7 +39,7 @@ async def document_upload(
|
||||
title = source.stem
|
||||
|
||||
# Copy file to case directory
|
||||
case_dir = config.CASES_DIR / case_number / "documents"
|
||||
case_dir = config.find_case_dir(case_number) / "documents" / "originals"
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = case_dir / source.name
|
||||
shutil.copy2(str(source), str(dest))
|
||||
@@ -68,7 +68,7 @@ async def document_upload(
|
||||
doc["doc_type"] = classified_type
|
||||
|
||||
# Git commit
|
||||
repo_dir = config.CASES_DIR / case_number
|
||||
repo_dir = config.find_case_dir(case_number)
|
||||
if repo_dir.exists():
|
||||
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
|
||||
doc_type_hebrew = {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db, embeddings
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db, embeddings, research_md
|
||||
from legal_mcp.services.lessons import (
|
||||
CITATION_GUIDANCE,
|
||||
DECISION_TEMPLATES,
|
||||
@@ -279,6 +281,32 @@ async def draft_section(
|
||||
return json.dumps(context, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def get_chair_directions(case_number: str) -> str:
|
||||
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר, לצורך יצירת direction_doc
|
||||
לכותב. קורא מ-analysis-and-research.md (שנוצר ע"י legal-analyst ומולא ע"י
|
||||
דפנה דרך ה-UI).
|
||||
|
||||
מחזיר JSON עם סטטוס, כמה סוגיות מולאו וכמה עדיין ריקות, ורשימה של עמדות
|
||||
מובנות — ניתן להזריק ישירות כ-direction_doc לבלוק י (דיון) ולבלוק יא (סיכום).
|
||||
|
||||
סטטוסים:
|
||||
missing — הקובץ לא קיים
|
||||
empty — הקובץ קיים אבל כל העמדות ריקות (טרם נקבעה דעה)
|
||||
partial — חלק מהעמדות מולאו
|
||||
complete — כל העמדות מולאו
|
||||
|
||||
אם המצב הוא `empty` או `missing` — הכותב צריך לעצור ולבקש מדפנה למלא
|
||||
את הקובץ דרך ה-UI לפני המשך הכתיבה.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
"""
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
file_path = case_dir / "documents" / "research" / "analysis-and-research.md"
|
||||
result = research_md.extract_chair_directions(file_path)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def get_decision_template(case_number: str) -> str:
|
||||
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ async def workflow_status(case_number: str) -> str:
|
||||
from pathlib import Path
|
||||
from legal_mcp import config
|
||||
|
||||
case_dir = config.CASES_DIR / case_number
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
draft_path = case_dir / "drafts" / "decision.md"
|
||||
has_draft = draft_path.exists()
|
||||
draft_size = draft_path.stat().st_size if has_draft else 0
|
||||
@@ -318,3 +318,97 @@ async def ingest_final_version(
|
||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||
except ValueError as e:
|
||||
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ── Chair feedback tools ──────────────────────────────────────────
|
||||
|
||||
|
||||
async def record_chair_feedback(
|
||||
case_number: str,
|
||||
feedback_text: str,
|
||||
block_id: str = "block-yod",
|
||||
category: str = "missing_content",
|
||||
lesson_extracted: str = "",
|
||||
) -> str:
|
||||
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
feedback_text: ההערה של דפנה (מה חסר, מה לא נכון, מה צריך לשנות)
|
||||
block_id: הבלוק שההערה מתייחסת אליו (ברירת מחדל: block-yod)
|
||||
category: קטגוריה — missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||
lesson_extracted: הלקח שהופק מההערה (אם ברור כבר)
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
case_id = UUID(case["id"]) if case else None
|
||||
|
||||
valid_categories = [
|
||||
"missing_content", "wrong_tone", "wrong_structure",
|
||||
"factual_error", "style", "other",
|
||||
]
|
||||
if category not in valid_categories:
|
||||
return f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}"
|
||||
|
||||
feedback_id = await db.record_chair_feedback(
|
||||
case_id=case_id,
|
||||
block_id=block_id,
|
||||
feedback_text=feedback_text,
|
||||
category=category,
|
||||
lesson_extracted=lesson_extracted,
|
||||
)
|
||||
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
"feedback_id": str(feedback_id),
|
||||
"message": f"הערה נרשמה בהצלחה. קטגוריה: {category}.",
|
||||
"next_steps": [
|
||||
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
||||
"כדי לסמן כמטופל: resolve_chair_feedback",
|
||||
],
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def list_chair_feedback(
|
||||
case_number: str = "",
|
||||
category: str = "",
|
||||
unresolved_only: bool = True,
|
||||
) -> str:
|
||||
"""הצגת הערות יו"ר שתועדו, עם אפשרות סינון.
|
||||
|
||||
Args:
|
||||
case_number: סינון לפי תיק (אם ריק — כל ההערות)
|
||||
category: סינון לפי קטגוריה
|
||||
unresolved_only: האם להציג רק הערות שלא טופלו (ברירת מחדל: כן)
|
||||
"""
|
||||
case_id = None
|
||||
if case_number:
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if case:
|
||||
case_id = UUID(case["id"])
|
||||
|
||||
feedbacks = await db.list_chair_feedback(
|
||||
case_id=case_id,
|
||||
category=category or None,
|
||||
unresolved_only=unresolved_only,
|
||||
)
|
||||
|
||||
if not feedbacks:
|
||||
return "אין הערות שמתאימות לסינון."
|
||||
|
||||
items = []
|
||||
for fb in feedbacks:
|
||||
items.append({
|
||||
"id": str(fb["id"]),
|
||||
"case_id": str(fb["case_id"]) if fb["case_id"] else None,
|
||||
"block_id": fb["block_id"],
|
||||
"category": fb["category"],
|
||||
"feedback": fb["feedback_text"],
|
||||
"lesson": fb["lesson_extracted"],
|
||||
"resolved": fb["resolved"],
|
||||
"date": fb["created_at"].isoformat() if fb.get("created_at") else None,
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"total": len(items),
|
||||
"feedbacks": items,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
65
paperclip-bug-report.md
Normal file
65
paperclip-bug-report.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Bug: Skill import from Gitea — wrong raw URL format causes empty SKILL.md
|
||||
|
||||
**File at:** https://github.com/paperclipai/paperclip/issues/new
|
||||
|
||||
## Title
|
||||
Skill import from Gitea: wrong raw URL format causes empty SKILL.md
|
||||
|
||||
## Body
|
||||
|
||||
### Bug Summary
|
||||
|
||||
When importing skills from a **Gitea** instance (self-hosted), Paperclip fetches the git tree successfully via the `/api/v3/` endpoint (which Gitea supports), but then uses the **wrong raw file URL format** to download `SKILL.md` content, resulting in a 404 and an almost-empty stub being saved.
|
||||
|
||||
### Environment
|
||||
|
||||
- Paperclip server: `@paperclipai/server@2026.403.0`
|
||||
- Gitea instance: self-hosted Gitea
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
1. Host a skill repo on a Gitea instance with a `SKILL.md` (32KB+), `scripts/`, and `references/` directories
|
||||
2. Import the skill via URL: `https://my-gitea.example.com/org/skill-name.git`
|
||||
3. Observe that only a stub SKILL.md (~283 bytes) is saved, and subdirectories are missing
|
||||
|
||||
### Root Cause
|
||||
|
||||
In `server/dist/services/github-fetch.js`, the `resolveRawGitHubUrl()` function builds:
|
||||
|
||||
```
|
||||
https://{hostname}/raw/{owner}/{repo}/{ref}/{file}
|
||||
```
|
||||
|
||||
This format works for **GitHub Enterprise**, but **not for Gitea**. Gitea expects:
|
||||
|
||||
```
|
||||
https://{hostname}/{owner}/{repo}/raw/branch/{ref}/{file}
|
||||
```
|
||||
|
||||
### Proof
|
||||
|
||||
```bash
|
||||
# Paperclip's URL format -> 404
|
||||
$ curl -s -o /dev/null -w "%{http_code}" "https://my-gitea.example.com/raw/org/skill-repo/main/SKILL.md"
|
||||
404
|
||||
|
||||
# Correct Gitea format -> 200
|
||||
$ curl -s -o /dev/null -w "%{http_code}" "https://my-gitea.example.com/org/skill-repo/raw/branch/main/SKILL.md"
|
||||
200
|
||||
```
|
||||
|
||||
### Secondary Issue
|
||||
|
||||
When `SKILL.md` is at the repository root, `path.posix.dirname("SKILL.md")` returns `"."`, causing the inventory filter `entry.startsWith("./")` to miss all sibling directories (`scripts/`, `references/`). This means even if the raw URL worked, subdirectories would still be excluded from the file inventory.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
1. **Detect Gitea** vs GitHub Enterprise (e.g., check for `/api/v1/` endpoint which is Gitea-specific, vs `/api/v3/`)
|
||||
2. **Use the correct raw URL format** per platform:
|
||||
- GitHub/GHE: `https://{hostname}/raw/{owner}/{repo}/{ref}/{file}`
|
||||
- Gitea: `https://{hostname}/{owner}/{repo}/raw/branch/{ref}/{file}`
|
||||
3. **Fix root-level SKILL.md inventory**: when `skillDir === "."`, include all files instead of filtering by `entry.startsWith("./")`
|
||||
|
||||
### Workaround
|
||||
|
||||
Manually clone the repo into `~/.paperclip/instances/default/skills/{company_id}/{slug}/` and update the `company_skills` table directly with correct markdown content and file_inventory.
|
||||
37
scripts/auto-sync-cases.sh
Executable file
37
scripts/auto-sync-cases.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Auto-sync case repos to Gitea
|
||||
# Runs via crontab every minute, commits and pushes any changes found.
|
||||
|
||||
CASES_DIR="/home/chaim/legal-ai/data/cases"
|
||||
LOG="/home/chaim/legal-ai/data/.auto-sync.log"
|
||||
GIT_ENV="GIT_AUTHOR_NAME=Ezer Mishpati GIT_AUTHOR_EMAIL=legal@local GIT_COMMITTER_NAME=Ezer Mishpati GIT_COMMITTER_EMAIL=legal@local GIT_TERMINAL_PROMPT=0"
|
||||
|
||||
for status_dir in "$CASES_DIR"/new "$CASES_DIR"/in-progress "$CASES_DIR"/completed; do
|
||||
[ -d "$status_dir" ] || continue
|
||||
for case_dir in "$status_dir"/*/; do
|
||||
[ -d "$case_dir/.git" ] || continue
|
||||
|
||||
cd "$case_dir" || continue
|
||||
|
||||
# Check for any changes (modified, new, deleted)
|
||||
changes=$(git status --porcelain 2>/dev/null)
|
||||
[ -z "$changes" ] && continue
|
||||
|
||||
# Stage all changes
|
||||
git add -A 2>/dev/null
|
||||
|
||||
# Build commit message from changed files
|
||||
changed_files=$(git diff --cached --name-only 2>/dev/null | head -5)
|
||||
count=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
||||
case_name=$(basename "$case_dir")
|
||||
msg="סנכרון אוטומטי — ${count} קבצים שונו"
|
||||
|
||||
# Commit
|
||||
env $GIT_ENV git commit -m "$msg" --quiet 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
# Push (non-blocking, ignore errors)
|
||||
git push origin main --quiet 2>/dev/null
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') | $case_name | $count files synced" >> "$LOG"
|
||||
fi
|
||||
done
|
||||
done
|
||||
163
scripts/backfill_pattern_frequency.py
Normal file
163
scripts/backfill_pattern_frequency.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""Backfill style_patterns.frequency with real occurrence counts.
|
||||
|
||||
The analyzer currently stores frequency=1 for every pattern (it only extracts
|
||||
unique patterns, doesn't count occurrences). This script scans the full_text
|
||||
of every decision in style_corpus and updates each pattern's frequency to
|
||||
the true count of decisions containing the pattern_text as a substring.
|
||||
|
||||
Run once after analysis, and again whenever new decisions are added.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
# Load env
|
||||
for line in (Path.home() / ".env").read_text().splitlines():
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
|
||||
sys.path.insert(0, "/home/chaim/legal-ai/mcp-server/src")
|
||||
|
||||
from legal_mcp.services import db as db_mod # noqa: E402
|
||||
|
||||
|
||||
def _strip_nikud(text: str) -> str:
|
||||
"""Remove Hebrew combining marks (nikud) for robust matching."""
|
||||
return "".join(
|
||||
c for c in unicodedata.normalize("NFD", text)
|
||||
if not unicodedata.combining(c)
|
||||
)
|
||||
|
||||
|
||||
def _extract_searchable_variants(pattern_text: str) -> list[str]:
|
||||
"""Extract searchable substrings from a pattern template.
|
||||
|
||||
The analyzer stores patterns as templates with:
|
||||
- Placeholders in [brackets]: "בפנינו ערר על החלטת [הגוף] מיום [תאריך]"
|
||||
- Alternatives separated by / : "נפנה ל... / ראה והשווה / נפנה להחלטה"
|
||||
- Ellipsis ... for variable parts
|
||||
|
||||
This function returns a list of concrete substrings to search for.
|
||||
We pick the longest fixed segment from each alternative (>= 4 chars)
|
||||
so that matching is specific enough to be meaningful but still flexible.
|
||||
"""
|
||||
# Split on " / " or " או " to get alternatives
|
||||
alternatives = re.split(r"\s*/\s*|\s+או\s+", pattern_text)
|
||||
|
||||
variants: list[str] = []
|
||||
for alt in alternatives:
|
||||
alt = alt.strip()
|
||||
if not alt:
|
||||
continue
|
||||
|
||||
# Remove bracket placeholders [X]
|
||||
alt = re.sub(r"\[[^\]]*\]", "|", alt)
|
||||
# Replace ellipsis with separator
|
||||
alt = re.sub(r"\.{2,}", "|", alt)
|
||||
# Remove ellipsis unicode
|
||||
alt = alt.replace("…", "|")
|
||||
|
||||
# Split on the | separator and take fixed segments
|
||||
segments = [s.strip(" ,.:;\"'") for s in alt.split("|")]
|
||||
# Keep segments long enough to be meaningful (>= 4 chars, not just common words)
|
||||
good = [s for s in segments if len(s) >= 4]
|
||||
|
||||
if good:
|
||||
# Use the longest segment as the key variant for this alternative
|
||||
variants.append(max(good, key=len))
|
||||
elif alt.strip():
|
||||
# Fallback: use the whole cleaned alternative
|
||||
stripped = alt.replace("|", " ").strip()
|
||||
if len(stripped) >= 4:
|
||||
variants.append(stripped)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
unique = []
|
||||
for v in variants:
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
unique.append(v)
|
||||
return unique
|
||||
|
||||
|
||||
def _count_decisions_containing(variants: list[str], normalized_decisions: list) -> int:
|
||||
"""Count how many decisions contain ANY of the variants."""
|
||||
count = 0
|
||||
for _, _, text in normalized_decisions:
|
||||
if any(v in text for v in variants):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
pool = await db_mod.get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
decisions = await conn.fetch(
|
||||
"SELECT id, decision_number, full_text FROM style_corpus "
|
||||
"WHERE full_text IS NOT NULL AND length(full_text) > 0"
|
||||
)
|
||||
patterns = await conn.fetch(
|
||||
"SELECT id, pattern_text, pattern_type FROM style_patterns"
|
||||
)
|
||||
|
||||
print(f"Scanning {len(patterns)} patterns across {len(decisions)} decisions...")
|
||||
|
||||
# Normalize decisions once
|
||||
normalized_decisions = [
|
||||
(d["id"], d["decision_number"], _strip_nikud(d["full_text"]))
|
||||
for d in decisions
|
||||
]
|
||||
|
||||
updates = []
|
||||
for p in patterns:
|
||||
pattern_text = p["pattern_text"]
|
||||
if not pattern_text or len(pattern_text) < 3:
|
||||
updates.append((0, p["id"]))
|
||||
continue
|
||||
|
||||
variants = _extract_searchable_variants(_strip_nikud(pattern_text))
|
||||
if not variants:
|
||||
updates.append((0, p["id"]))
|
||||
continue
|
||||
|
||||
count = _count_decisions_containing(variants, normalized_decisions)
|
||||
updates.append((count, p["id"]))
|
||||
|
||||
await conn.executemany(
|
||||
"UPDATE style_patterns SET frequency = $1 WHERE id = $2",
|
||||
updates,
|
||||
)
|
||||
|
||||
# Show distribution
|
||||
rows = await conn.fetch(
|
||||
"SELECT pattern_type, pattern_text, frequency "
|
||||
"FROM style_patterns "
|
||||
"ORDER BY frequency DESC "
|
||||
"LIMIT 15"
|
||||
)
|
||||
print(f"\nTop 15 patterns by real frequency:")
|
||||
for r in rows:
|
||||
print(f" {r['frequency']:>3} [{r['pattern_type']:<22}] {r['pattern_text'][:90]}")
|
||||
|
||||
dist = await conn.fetch(
|
||||
"SELECT frequency, count(*) FROM style_patterns "
|
||||
"GROUP BY frequency ORDER BY frequency DESC"
|
||||
)
|
||||
print(f"\nFrequency distribution:")
|
||||
for r in dist:
|
||||
print(f" frequency={r['frequency']:>3} → {r['count']} patterns")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
349
scripts/batch_upload_training.py
Normal file
349
scripts/batch_upload_training.py
Normal file
@@ -0,0 +1,349 @@
|
||||
"""Batch upload proofread training corpus to style DB.
|
||||
|
||||
Two-phase workflow:
|
||||
--preview Extract metadata from all .md files, print review table, don't upload
|
||||
--upload Actually upload all files (with optional --only FILE to run one)
|
||||
|
||||
Metadata extraction:
|
||||
* decision_number: from filename (ARAR-YY-NNNN / ערר NNNN-YY) or decision date year
|
||||
* decision_date: from "ניתנה ... <day> ב<Hebrew month> <YYYY>" near end of text
|
||||
* categories: keyword heuristics on body text
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROOFREAD_DIR = Path("/home/chaim/legal-ai/data/training/proofread")
|
||||
|
||||
# Manual metadata overrides for files where auto-extraction can't determine values.
|
||||
METADATA_OVERRIDES: dict[str, dict] = {
|
||||
"ARAR-25-1067 - יחיעם יפה ואח׳.md": {
|
||||
"decision_date": "2025-11-27", # no "ניתנה" signature in file; user-provided
|
||||
},
|
||||
}
|
||||
|
||||
# Files to skip — already in style_corpus from legacy ingestion
|
||||
# (verified by exact character-count match with existing DB rows).
|
||||
SKIP_FILES = {
|
||||
"תמא 38-בית הכרם-1126+1141-החלטה.md", # → corpus: 1126/1141
|
||||
"היתר בניה-בית שמש-1180+1181-החלטה.md", # → corpus: 1180/1181
|
||||
"היתר בניה-הראל-1043+1054-החלטה.md", # → corpus: 1043/1054
|
||||
"היתר בניה-הראל-1071+1077-החלטה.md", # → corpus: 1071/1077
|
||||
}
|
||||
|
||||
# Load env vars needed by mcp-server
|
||||
ENV_FILE = Path.home() / ".env"
|
||||
if ENV_FILE.exists():
|
||||
for line in ENV_FILE.read_text().splitlines():
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
|
||||
# Make mcp-server package importable
|
||||
sys.path.insert(0, "/home/chaim/legal-ai/mcp-server/src")
|
||||
|
||||
|
||||
# ── Decision number extraction ───────────────────────────────────
|
||||
|
||||
FILENAME_NUMBER_PATTERNS = [
|
||||
# ARAR-YY-NNNN[-X] - title.md
|
||||
re.compile(r"^ARAR-(\d{2})-(\d{3,4})"),
|
||||
# ערר NNNN-YY title.md or ערר NNNN-YY title
|
||||
re.compile(r"^ערר\s+(\d{3,4})-(\d{2})"),
|
||||
# ערר NNNN - title (no year in filename — needs date lookup)
|
||||
re.compile(r"^ערר\s+(\d{3,4})\s*-"),
|
||||
]
|
||||
|
||||
LEGACY_MULTI_PATTERN = re.compile(r"(\d{3,4})\+(\d{3,4})")
|
||||
|
||||
|
||||
def decision_number_from_filename(stem: str) -> tuple[str | None, str | None]:
|
||||
"""Return (number, year_short) or (multi_number, None) or (None, None).
|
||||
|
||||
year_short is YY (last 2 digits) if extractable from filename.
|
||||
For legacy files with 'NNNN+NNNN' or no year, returns partial info
|
||||
that must be completed from decision date.
|
||||
"""
|
||||
# ARAR-YY-NNNN
|
||||
m = FILENAME_NUMBER_PATTERNS[0].match(stem)
|
||||
if m:
|
||||
year, num = m.group(1), m.group(2)
|
||||
return f"{num}/{year}", year
|
||||
|
||||
# ערר NNNN-YY
|
||||
m = FILENAME_NUMBER_PATTERNS[1].match(stem)
|
||||
if m:
|
||||
num, year = m.group(1), m.group(2)
|
||||
return f"{num}/{year}", year
|
||||
|
||||
# ערר NNNN - title (no year)
|
||||
m = FILENAME_NUMBER_PATTERNS[2].match(stem)
|
||||
if m:
|
||||
num = m.group(1)
|
||||
return f"{num}/??", None
|
||||
|
||||
# Legacy: "NNNN+NNNN" merged decisions
|
||||
m = LEGACY_MULTI_PATTERN.search(stem)
|
||||
if m:
|
||||
return f"{m.group(1)}+{m.group(2)}/??", None
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
# ── Decision date extraction ─────────────────────────────────────
|
||||
|
||||
HEBREW_MONTHS = {
|
||||
"ינואר": 1, "בינואר": 1,
|
||||
"פברואר": 2, "בפברואר": 2,
|
||||
"מרץ": 3, "מרס": 3, "במרץ": 3, "במרס": 3,
|
||||
"אפריל": 4, "באפריל": 4,
|
||||
"מאי": 5, "במאי": 5,
|
||||
"יוני": 6, "ביוני": 6,
|
||||
"יולי": 7, "ביולי": 7,
|
||||
"אוגוסט": 8, "באוגוסט": 8,
|
||||
"ספטמבר": 9, "בספטמבר": 9,
|
||||
"אוקטובר": 10, "באוקטובר": 10,
|
||||
"נובמבר": 11, "בנובמבר": 11,
|
||||
"דצמבר": 12, "בדצמבר": 12,
|
||||
}
|
||||
|
||||
# Matches "<day> ב<month>, <year>" or "<day> <month>, <year>" (with optional commas)
|
||||
DATE_RE = re.compile(
|
||||
r"(\d{1,2})\s+(ב?(?:ינואר|פברואר|מרץ|מרס|אפריל|מאי|יוני|יולי|אוגוסט|ספטמבר|אוקטובר|נובמבר|דצמבר))\s*[,.]?\s*(\d{4})"
|
||||
)
|
||||
|
||||
NITNA_RE = re.compile(r"ניתנ[הו]?\s+(?:פה\s+אחד|בדעת\s+רוב|היום)?")
|
||||
|
||||
|
||||
def decision_date_from_text(text: str) -> str | None:
|
||||
"""Extract decision date in YYYY-MM-DD format from 'ניתנה... DATE' section.
|
||||
|
||||
Searches the last ~2000 chars where the signing block lives.
|
||||
"""
|
||||
tail = text[-2500:] if len(text) > 2500 else text
|
||||
|
||||
# Prefer dates near "ניתנה" marker
|
||||
nitna_match = NITNA_RE.search(tail)
|
||||
search_text = tail[nitna_match.start():] if nitna_match else tail
|
||||
|
||||
m = DATE_RE.search(search_text)
|
||||
if not m:
|
||||
# Fall back: search whole tail
|
||||
m = DATE_RE.search(tail)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
day = int(m.group(1))
|
||||
month = HEBREW_MONTHS.get(m.group(2))
|
||||
year = int(m.group(3))
|
||||
if not month:
|
||||
return None
|
||||
try:
|
||||
from datetime import date
|
||||
return date(year, month, day).isoformat()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# ── Subject category extraction ──────────────────────────────────
|
||||
|
||||
# Categories as defined in the tool signature.
|
||||
ALL_CATEGORIES = [
|
||||
"בנייה", "שימוש חורג", "תכנית", "היתר", "הקלה",
|
||||
"חלוקה", 'תמ"א 38', "היטל השבחה", "פיצויים 197",
|
||||
]
|
||||
|
||||
|
||||
def categorize(text: str) -> list[str]:
|
||||
"""Heuristic category detection based on subject matter, not incidental mentions.
|
||||
|
||||
Strategy: the real subject is established in the opening 2000 chars
|
||||
(first decision-opening paragraph). Secondary signal is repetition count
|
||||
— casual mentions in law citations don't repeat.
|
||||
"""
|
||||
opening = text[:2000] # subject is stated up front
|
||||
t = text
|
||||
|
||||
cats: list[str] = []
|
||||
|
||||
# תמ"א 38 — very specific marker, single mention is fine
|
||||
if re.search(r'תמ[״"\']?א\s*38|תמא\s*38', t):
|
||||
cats.append('תמ"א 38')
|
||||
|
||||
# היטל השבחה — require real engagement: must appear in opening OR 3+ times
|
||||
hsbacha_count = len(re.findall(r"היטל(?:י)?\s+השבחה", t))
|
||||
if hsbacha_count >= 3 or re.search(r"היטל(?:י)?\s+השבחה", opening):
|
||||
cats.append("היטל השבחה")
|
||||
|
||||
# פיצויים 197 — require multiple mentions OR in opening
|
||||
p197_re = r"פיצויים\s+לפי\s+(?:ס(?:עיף|')\s*)?197|סעיף\s*197|ס['\"]?\s*197"
|
||||
p197_count = len(re.findall(p197_re, t))
|
||||
if p197_count >= 2 or re.search(p197_re, opening):
|
||||
cats.append("פיצויים 197")
|
||||
|
||||
# שימוש חורג — must appear in opening OR 3+ times (avoids law-quote false positives)
|
||||
shimush_count = t.count("שימוש חורג")
|
||||
if shimush_count >= 3 or "שימוש חורג" in opening:
|
||||
cats.append("שימוש חורג")
|
||||
|
||||
# הקלה — real subject if 3+ mentions AND appears in opening
|
||||
hakala_count = len(re.findall(r"\bהקלה\b|\bהקלות\b", t))
|
||||
if hakala_count >= 3 and re.search(r"\bהקלה\b|\bהקלות\b", opening):
|
||||
cats.append("הקלה")
|
||||
|
||||
# חלוקה — "איחוד וחלוקה" or "חלוקה חדשה" (specific phrases)
|
||||
if re.search(r"איחוד\s+וחלוקה|חלוקה\s+חדשה|תכנית\s+לחלוקה", t):
|
||||
cats.append("חלוקה")
|
||||
|
||||
# תכנית — plan-level appeal (primary subject). Allow ה/ב/ל prefixes on תכנית.
|
||||
tochnit_opening = bool(re.search(
|
||||
r"הפקדת\s+ה?תכנית|"
|
||||
r"אישור\s+ה?תכנית|"
|
||||
r"המלצה\s+להפקיד|"
|
||||
r"להפקיד\s+את\s+ה?תכנית|"
|
||||
r"לדון\s+בתכנית|"
|
||||
r"דנה\s+בתכנית|"
|
||||
r"החלטה\s+לאשר\s+ה?תכנית",
|
||||
opening,
|
||||
))
|
||||
if tochnit_opening:
|
||||
cats.append("תכנית")
|
||||
|
||||
# היתר — "בקשה להיתר" or "היתר בניה" as subject in opening
|
||||
if re.search(r"בקשה\s+להיתר|היתר\s+בני(?:י)?ה", opening):
|
||||
cats.append("היתר")
|
||||
|
||||
# בנייה — default/fallback for building-permit cases
|
||||
# (not for plan-level תכנית-only cases)
|
||||
has_permit_subject = "היתר" in cats or "הקלה" in cats or 'תמ"א 38' in cats
|
||||
if has_permit_subject and "בנייה" not in cats:
|
||||
cats.append("בנייה")
|
||||
|
||||
# If nothing matched, default to בנייה
|
||||
return cats or ["בנייה"]
|
||||
|
||||
|
||||
# ── Year fallback from date ──────────────────────────────────────
|
||||
|
||||
|
||||
def finalize_decision_number(number: str | None, date_iso: str | None) -> str:
|
||||
"""If filename number is missing year, fill it from decision date."""
|
||||
if not number:
|
||||
if date_iso:
|
||||
# Extract last 2 digits of Hebrew year via Gregorian year
|
||||
return f"??/{date_iso[2:4]}"
|
||||
return ""
|
||||
if number.endswith("/??"):
|
||||
if date_iso:
|
||||
yy = date_iso[2:4]
|
||||
return number.replace("/??", f"/{yy}")
|
||||
return number.replace("/??", "")
|
||||
return number
|
||||
|
||||
|
||||
# ── Main metadata extraction ─────────────────────────────────────
|
||||
|
||||
|
||||
def extract_metadata(path: Path) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
num_from_name, _ = decision_number_from_filename(path.stem)
|
||||
date_iso = decision_date_from_text(text)
|
||||
decision_number = finalize_decision_number(num_from_name, date_iso)
|
||||
cats = categorize(text)
|
||||
meta = {
|
||||
"file": path.name,
|
||||
"decision_number": decision_number,
|
||||
"decision_date": date_iso or "??",
|
||||
"categories": cats,
|
||||
"chars": len(text),
|
||||
}
|
||||
# Apply manual overrides
|
||||
if path.name in METADATA_OVERRIDES:
|
||||
meta.update(METADATA_OVERRIDES[path.name])
|
||||
return meta
|
||||
|
||||
|
||||
def print_preview(results: list[dict]) -> None:
|
||||
"""Print review table of metadata for all files."""
|
||||
print(f"\n{'#':<3} {'FILE':<55} {'NUMBER':<15} {'DATE':<12} {'CATEGORIES'}")
|
||||
print("-" * 130)
|
||||
for i, r in enumerate(results, 1):
|
||||
file_short = r["file"] if len(r["file"]) <= 53 else r["file"][:50] + "..."
|
||||
cats = ", ".join(r["categories"])
|
||||
print(f"{i:<3} {file_short:<55} {r['decision_number']:<15} {r['decision_date']:<12} {cats}")
|
||||
print()
|
||||
# Highlight issues
|
||||
issues = [r for r in results if r["decision_date"] == "??" or not r["decision_number"] or "??" in r["decision_number"]]
|
||||
if issues:
|
||||
print(f"⚠️ {len(issues)} files with incomplete metadata:")
|
||||
for r in issues:
|
||||
print(f" - {r['file']} → number={r['decision_number']!r} date={r['decision_date']!r}")
|
||||
|
||||
|
||||
# ── Upload ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def upload_one(meta: dict) -> dict:
|
||||
from legal_mcp.tools.documents import document_upload_training
|
||||
|
||||
path = PROOFREAD_DIR / meta["file"]
|
||||
result = await document_upload_training(
|
||||
file_path=str(path),
|
||||
decision_number=meta["decision_number"],
|
||||
decision_date=meta["decision_date"] if meta["decision_date"] != "??" else "",
|
||||
subject_categories=meta["categories"],
|
||||
title=path.stem,
|
||||
)
|
||||
return {"file": meta["file"], "result": result}
|
||||
|
||||
|
||||
async def upload_all(results: list[dict]) -> None:
|
||||
for i, meta in enumerate(results, 1):
|
||||
try:
|
||||
r = await upload_one(meta)
|
||||
print(f"[{i}/{len(results)}] ✓ {meta['file']}")
|
||||
print(f" {r['result'][:200]}")
|
||||
except Exception as e:
|
||||
print(f"[{i}/{len(results)}] ✗ {meta['file']}: {e}")
|
||||
|
||||
|
||||
# ── CLI ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--preview", action="store_true", help="Show metadata table without uploading")
|
||||
ap.add_argument("--upload", action="store_true", help="Upload all files to style corpus")
|
||||
ap.add_argument("--only", help="Only process this specific filename")
|
||||
args = ap.parse_args()
|
||||
|
||||
files = sorted(PROOFREAD_DIR.glob("*.md"))
|
||||
files = [f for f in files if f.name not in SKIP_FILES]
|
||||
if args.only:
|
||||
files = [f for f in files if f.name == args.only]
|
||||
if not files:
|
||||
print(f"File not found: {args.only}")
|
||||
return 1
|
||||
|
||||
results = [extract_metadata(f) for f in files]
|
||||
|
||||
if args.preview or not args.upload:
|
||||
print_preview(results)
|
||||
if not args.upload:
|
||||
return 0
|
||||
|
||||
if args.upload:
|
||||
print(f"\n>>> Uploading {len(results)} files to style corpus...\n")
|
||||
asyncio.run(upload_all(results))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
232
scripts/benchmark_embeddings.py
Normal file
232
scripts/benchmark_embeddings.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""Benchmark embedding models on case 1130-25 documents.
|
||||
|
||||
Compares voyage-3-large (current), voyage-4-large, and voyage-law-2
|
||||
on Hebrew legal text retrieval quality, timing, and cost.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import voyageai
|
||||
|
||||
API_KEY = os.environ.get("VOYAGE_API_KEY", "pa-qbfhBDxW0tVtgzr_abMyw_AJO2gli9w3nnqyHuQOW-e")
|
||||
client = voyageai.Client(api_key=API_KEY)
|
||||
|
||||
MODELS = [
|
||||
"voyage-3-large", # current
|
||||
"voyage-4-large", # upgrade candidate
|
||||
"voyage-law-2", # legal specialist
|
||||
]
|
||||
|
||||
# Pricing per 1M tokens (from Voyage AI docs)
|
||||
PRICING = {
|
||||
"voyage-3-large": 0.06,
|
||||
"voyage-4-large": 0.12,
|
||||
"voyage-law-2": 0.12,
|
||||
}
|
||||
|
||||
DOCS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents")
|
||||
|
||||
DOCUMENTS = {
|
||||
"כתב ערר קובר": DOCS_DIR / "2025-08-14-כתב-ערר-קובר.md",
|
||||
"כתב ערר מטמון": DOCS_DIR / "2025-10-22-כתב-ערר-מטמון.md",
|
||||
"תשובת ועדת הראל": DOCS_DIR / "2025-09-02-כתב-תשובה-ועדת-הראל-לערר.md",
|
||||
"תשובת ליבמן": DOCS_DIR / "2025-09-01-כתב-תשובה-ליבמן-לערר.md",
|
||||
}
|
||||
|
||||
# Test queries — real questions a judge would ask about this case
|
||||
QUERIES = [
|
||||
"מהי הטענה המרכזית של העוררים בנוגע לחניה?",
|
||||
"מה עמדת הוועדה המקומית לגבי התכנית?",
|
||||
"האם יש פגיעה בזכויות הבנייה של השכנים?",
|
||||
"מהם התנאים שנקבעו בהיתר הבנייה?",
|
||||
"האם התכנית עומדת בתקן החניה?",
|
||||
"מה טענות המשיבים לגבי הגובה והצפיפות?",
|
||||
"האם נערך שימוע כדין לפני מתן ההחלטה?",
|
||||
"מהם הנימוקים לאישור התכנית על ידי הוועדה המקומית?",
|
||||
]
|
||||
|
||||
|
||||
def chunk_text(text: str, chunk_size: int = 600, overlap: int = 100) -> list[str]:
|
||||
"""Simple word-based chunking."""
|
||||
words = text.split()
|
||||
chunks = []
|
||||
i = 0
|
||||
while i < len(words):
|
||||
chunk = " ".join(words[i:i + chunk_size])
|
||||
chunks.append(chunk)
|
||||
i += chunk_size - overlap
|
||||
return chunks
|
||||
|
||||
|
||||
def cosine_sim(a: list[float], b: list[float]) -> float:
|
||||
dot = sum(x * y for x, y in zip(a, b))
|
||||
norm_a = sum(x * x for x in a) ** 0.5
|
||||
norm_b = sum(x * x for x in b) ** 0.5
|
||||
return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0
|
||||
|
||||
|
||||
def main():
|
||||
# Load and chunk documents
|
||||
print("=" * 70)
|
||||
print("Loading and chunking documents...")
|
||||
print("=" * 70)
|
||||
|
||||
all_chunks = [] # (doc_name, chunk_index, text)
|
||||
for doc_name, doc_path in DOCUMENTS.items():
|
||||
text = doc_path.read_text(encoding="utf-8")
|
||||
chunks = chunk_text(text)
|
||||
for i, chunk in enumerate(chunks):
|
||||
all_chunks.append((doc_name, i, chunk))
|
||||
print(f" {doc_name}: {len(text):,} chars, {len(text.split()):,} words -> {len(chunks)} chunks")
|
||||
|
||||
chunk_texts = [c[2] for c in all_chunks]
|
||||
total_chunks = len(chunk_texts)
|
||||
print(f"\nTotal: {total_chunks} chunks")
|
||||
|
||||
# Estimate tokens (rough: 1 Hebrew word ~ 2-3 tokens)
|
||||
total_words = sum(len(t.split()) for t in chunk_texts)
|
||||
est_tokens_docs = int(total_words * 2.5)
|
||||
total_query_words = sum(len(q.split()) for q in QUERIES)
|
||||
est_tokens_queries = int(total_query_words * 2.5)
|
||||
|
||||
print(f"Estimated tokens per model: ~{est_tokens_docs:,} (docs) + ~{est_tokens_queries:,} (queries)")
|
||||
|
||||
results = {}
|
||||
|
||||
for model in MODELS:
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"Model: {model}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
# Embed documents
|
||||
print(f" Embedding {total_chunks} chunks...")
|
||||
t0 = time.time()
|
||||
doc_embeddings = client.embed(
|
||||
chunk_texts,
|
||||
model=model,
|
||||
input_type="document",
|
||||
)
|
||||
doc_time = time.time() - t0
|
||||
doc_usage = doc_embeddings.total_tokens
|
||||
doc_embs = doc_embeddings.embeddings
|
||||
print(f" Done in {doc_time:.1f}s — {doc_usage:,} tokens used")
|
||||
|
||||
# Embed queries
|
||||
print(f" Embedding {len(QUERIES)} queries...")
|
||||
t0 = time.time()
|
||||
query_embeddings = client.embed(
|
||||
QUERIES,
|
||||
model=model,
|
||||
input_type="query",
|
||||
)
|
||||
query_time = time.time() - t0
|
||||
query_usage = query_embeddings.total_tokens
|
||||
query_embs = query_embeddings.embeddings
|
||||
print(f" Done in {query_time:.1f}s — {query_usage:,} tokens used")
|
||||
|
||||
total_tokens = doc_usage + query_usage
|
||||
cost = total_tokens / 1_000_000 * PRICING[model]
|
||||
|
||||
# Search: for each query, rank chunks by similarity
|
||||
print(f"\n Search results:")
|
||||
query_results = []
|
||||
for qi, query in enumerate(QUERIES):
|
||||
scores = []
|
||||
for ci, doc_emb in enumerate(doc_embs):
|
||||
sim = cosine_sim(query_embs[qi], doc_emb)
|
||||
scores.append((sim, all_chunks[ci][0], all_chunks[ci][1], all_chunks[ci][2][:80]))
|
||||
scores.sort(reverse=True)
|
||||
top5 = scores[:5]
|
||||
query_results.append({
|
||||
"query": query,
|
||||
"top5": [(s[0], s[1], s[2], s[3]) for s in top5],
|
||||
})
|
||||
print(f"\n Q{qi+1}: {query}")
|
||||
for rank, (score, doc_name, chunk_idx, preview) in enumerate(top5):
|
||||
print(f" #{rank+1} [{score:.4f}] {doc_name} (chunk {chunk_idx}): {preview}...")
|
||||
|
||||
results[model] = {
|
||||
"doc_time": doc_time,
|
||||
"query_time": query_time,
|
||||
"doc_tokens": doc_usage,
|
||||
"query_tokens": query_usage,
|
||||
"total_tokens": total_tokens,
|
||||
"cost_usd": cost,
|
||||
"dimensions": len(doc_embs[0]),
|
||||
"query_results": query_results,
|
||||
}
|
||||
|
||||
# Summary comparison
|
||||
print(f"\n{'=' * 70}")
|
||||
print("SUMMARY")
|
||||
print(f"{'=' * 70}")
|
||||
print(f"\n{'Model':<25} {'Tokens':>10} {'Time':>8} {'Cost':>10} {'Dims':>6}")
|
||||
print("-" * 65)
|
||||
for model in MODELS:
|
||||
r = results[model]
|
||||
print(f"{model:<25} {r['total_tokens']:>10,} {r['doc_time']+r['query_time']:>7.1f}s ${r['cost_usd']:>8.5f} {r['dimensions']:>6}")
|
||||
|
||||
# Compare top-1 agreement between models
|
||||
print(f"\n{'=' * 70}")
|
||||
print("TOP-1 AGREEMENT (which doc is ranked #1 for each query)")
|
||||
print(f"{'=' * 70}")
|
||||
print(f"\n{'Query':<50}", end="")
|
||||
for model in MODELS:
|
||||
print(f" {model.split('-')[-1]:>10}", end="")
|
||||
print()
|
||||
print("-" * 85)
|
||||
|
||||
for qi, query in enumerate(QUERIES):
|
||||
short_q = query[:48]
|
||||
print(f"{short_q:<50}", end="")
|
||||
for model in MODELS:
|
||||
top1_doc = results[model]["query_results"][qi]["top5"][0][1]
|
||||
# Shorten doc name
|
||||
short_doc = top1_doc[:10]
|
||||
print(f" {short_doc:>10}", end="")
|
||||
print()
|
||||
|
||||
# Score distribution comparison
|
||||
print(f"\n{'=' * 70}")
|
||||
print("AVERAGE TOP-5 SCORES PER MODEL")
|
||||
print(f"{'=' * 70}")
|
||||
for model in MODELS:
|
||||
all_top5_scores = []
|
||||
for qr in results[model]["query_results"]:
|
||||
for score, _, _, _ in qr["top5"]:
|
||||
all_top5_scores.append(score)
|
||||
avg = sum(all_top5_scores) / len(all_top5_scores)
|
||||
top1_scores = [qr["top5"][0][0] for qr in results[model]["query_results"]]
|
||||
avg_top1 = sum(top1_scores) / len(top1_scores)
|
||||
print(f" {model:<25} avg top-1: {avg_top1:.4f} avg top-5: {avg:.4f}")
|
||||
|
||||
# Save full results
|
||||
output_path = Path("/home/chaim/legal-ai/data/benchmark-embeddings.json")
|
||||
serializable = {}
|
||||
for model, r in results.items():
|
||||
serializable[model] = {
|
||||
"doc_time": r["doc_time"],
|
||||
"query_time": r["query_time"],
|
||||
"doc_tokens": r["doc_tokens"],
|
||||
"query_tokens": r["query_tokens"],
|
||||
"total_tokens": r["total_tokens"],
|
||||
"cost_usd": r["cost_usd"],
|
||||
"dimensions": r["dimensions"],
|
||||
"queries": [
|
||||
{
|
||||
"query": qr["query"],
|
||||
"top5": [{"score": s, "doc": d, "chunk": c, "preview": p} for s, d, c, p in qr["top5"]],
|
||||
}
|
||||
for qr in r["query_results"]
|
||||
],
|
||||
}
|
||||
output_path.write_text(json.dumps(serializable, ensure_ascii=False, indent=2))
|
||||
print(f"\nFull results saved to {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
203
scripts/benchmark_new_vs_old.py
Normal file
203
scripts/benchmark_new_vs_old.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Compare Google Vision extractions vs existing MDs, then benchmark voyage-law-2."""
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import voyageai
|
||||
|
||||
API_KEY = "pa-qbfhBDxW0tVtgzr_abMyw_AJO2gli9w3nnqyHuQOW-e"
|
||||
client = voyageai.Client(api_key=API_KEY)
|
||||
MODEL = "voyage-law-2"
|
||||
|
||||
DOCS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents")
|
||||
GOOGLE_DIR = DOCS_DIR / "extracted"
|
||||
|
||||
# Map new (Google Vision) files to existing MDs
|
||||
PAIRS = [
|
||||
("מרק קובר-כתב ערר.md", "2025-08-14-כתב-ערר-קובר.md"),
|
||||
("תשובה לערר מטעם המשיבים.md", "2025-09-01-כתב-תשובה-ליבמן-לערר.md"),
|
||||
("תשובת הועדה המרחבית לערר.md", "2025-09-02-כתב-תשובה-ועדת-הראל-לערר.md"),
|
||||
("תשובת המשיב-יצחק מטמון.md", "2025-10-22-כתב-ערר-מטמון.md"),
|
||||
("השלמת טיעון מטעם משיבים 2-3.md", "2025-12-23-השלמת-טיעון-ליבמן.md"),
|
||||
("תשובה מטעם העורר להשלמת טיעון.md", "2025-12-08-תגובת-קובר-לבקשת-השלמת-טיעון.md"),
|
||||
("בקשה להשלמת טיעון ממשיבים 2-3.md", "2025-12-03-בקשה-להשלמת-טיעון-ליבמן.md"),
|
||||
("השלמת טיעון מטעם הוועדה המקומית.md", "2026-02-04-השלמת-טיעון-ועדת-הראל.md"),
|
||||
("תגובת העורר לתשובת ועדת הראל להשלמת הטיעון ערר.md", "2026-02-10-תגובת-קובר-להשלמת-טיעון-הראל.md"),
|
||||
("כתב תשובה-השלמת טיעון מטעם המשיב יצחק מטמון.md", "2026-02-12-כתב-תשובה-השלמת-טיעון-מטמון.md"),
|
||||
("בקשת העורר לדחיית השלמת הטיעון במלואה.md", "2026-01-13-תגובת-קובר-לדחיית-השלמת-טיעון.md"),
|
||||
("1130-25-החלטה לתיקון פרוטוקול.md", "2025-11-27-החלטה-לתיקון-פרוטוקול.md"),
|
||||
("החלטת ביניים 1130-25.md", "2025-12-31-החלטת-ביניים.md"),
|
||||
("1130-25-פרוטוקול ועדת ערר והחלטה.md", "2025-10-27-פרוטוקול-דיון-ועדת-ערר.md"),
|
||||
("פרוטוקול ועדה מקומית לדיון בתכנית 152-1257682.md", "2025-07-23-פרוטוקול-ועדה-מקומית-הראל.md"),
|
||||
]
|
||||
|
||||
QUERIES = [
|
||||
"מהי הטענה המרכזית של העוררים בנוגע לחניה?",
|
||||
"מה עמדת הוועדה המקומית לגבי התכנית?",
|
||||
"האם יש פגיעה בזכויות הבנייה של השכנים?",
|
||||
"מהם התנאים שנקבעו בהיתר הבנייה?",
|
||||
"האם התכנית עומדת בתקן החניה?",
|
||||
"מה טענות המשיבים לגבי הגובה והצפיפות?",
|
||||
"האם נערך שימוע כדין לפני מתן ההחלטה?",
|
||||
"מהם הנימוקים לאישור התכנית על ידי הוועדה המקומית?",
|
||||
]
|
||||
|
||||
|
||||
def cosine_sim(a, b):
|
||||
dot = sum(x * y for x, y in zip(a, b))
|
||||
na = sum(x * x for x in a) ** 0.5
|
||||
nb = sum(x * x for x in b) ** 0.5
|
||||
return dot / (na * nb) if na and nb else 0.0
|
||||
|
||||
|
||||
def chunk_text(text, chunk_size=600, overlap=100):
|
||||
words = text.split()
|
||||
chunks = []
|
||||
i = 0
|
||||
while i < len(words):
|
||||
chunks.append(" ".join(words[i:i + chunk_size]))
|
||||
i += chunk_size - overlap
|
||||
return chunks
|
||||
|
||||
|
||||
def word_overlap(a, b):
|
||||
wa, wb = set(a.split()), set(b.split())
|
||||
if not wa or not wb:
|
||||
return 0.0
|
||||
return len(wa & wb) / max(len(wa), len(wb))
|
||||
|
||||
|
||||
def main():
|
||||
# ── Part 1: Document comparison ──
|
||||
print("=" * 70)
|
||||
print("PART 1: DOCUMENT COMPARISON (Google Vision vs Existing)")
|
||||
print("=" * 70)
|
||||
|
||||
comparison_results = []
|
||||
all_new_chunks = []
|
||||
all_old_chunks = []
|
||||
|
||||
for new_name, old_name in PAIRS:
|
||||
new_path = GOOGLE_DIR / new_name
|
||||
old_path = DOCS_DIR / old_name
|
||||
|
||||
if not new_path.exists():
|
||||
continue
|
||||
if not old_path.exists():
|
||||
print(f" SKIP (no existing): {old_name}")
|
||||
continue
|
||||
|
||||
new_text = new_path.read_text(encoding="utf-8")
|
||||
old_text = old_path.read_text(encoding="utf-8")
|
||||
|
||||
new_words = len(new_text.split())
|
||||
old_words = len(old_text.split())
|
||||
overlap = word_overlap(new_text, old_text)
|
||||
|
||||
short_name = old_name[:40]
|
||||
diff = new_words - old_words
|
||||
diff_pct = (diff / old_words * 100) if old_words else 0
|
||||
|
||||
comparison_results.append({
|
||||
"name": short_name,
|
||||
"old_words": old_words,
|
||||
"new_words": new_words,
|
||||
"diff": diff,
|
||||
"diff_pct": diff_pct,
|
||||
"overlap": overlap,
|
||||
})
|
||||
|
||||
# Chunk for embedding
|
||||
new_chunks = chunk_text(new_text)
|
||||
old_chunks = chunk_text(old_text)
|
||||
for i, c in enumerate(new_chunks):
|
||||
all_new_chunks.append((short_name, i, c))
|
||||
for i, c in enumerate(old_chunks):
|
||||
all_old_chunks.append((short_name, i, c))
|
||||
|
||||
print(f"\n{'Document':<42} {'Old':>6} {'New':>6} {'Diff':>8} {'Overlap':>8}")
|
||||
print("-" * 72)
|
||||
for r in comparison_results:
|
||||
print(f" {r['name']:<40} {r['old_words']:>6} {r['new_words']:>6} {r['diff']:>+7} ({r['diff_pct']:>+.0f}%) {r['overlap']:>7.0%}")
|
||||
|
||||
# ── Part 2: Embedding benchmark ──
|
||||
print(f"\n{'=' * 70}")
|
||||
print("PART 2: VOYAGE-LAW-2 EMBEDDING BENCHMARK")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
new_texts = [c[2] for c in all_new_chunks]
|
||||
old_texts = [c[2] for c in all_old_chunks]
|
||||
|
||||
print(f"\nNew chunks: {len(new_texts)}, Old chunks: {len(old_texts)}")
|
||||
|
||||
def embed_batched(texts, label):
|
||||
BATCH = 20
|
||||
all_embs = []
|
||||
total_tokens = 0
|
||||
t0 = time.time()
|
||||
for i in range(0, len(texts), BATCH):
|
||||
batch = texts[i:i+BATCH]
|
||||
result = client.embed(batch, model=MODEL, input_type="document")
|
||||
all_embs.extend(result.embeddings)
|
||||
total_tokens += result.total_tokens
|
||||
elapsed = time.time() - t0
|
||||
print(f" {label}: {len(texts)} chunks, {total_tokens:,} tokens, {elapsed:.1f}s")
|
||||
return all_embs, total_tokens, elapsed
|
||||
|
||||
# Embed new
|
||||
print("Embedding NEW (Google Vision) chunks...")
|
||||
new_embs, new_tokens, new_time = embed_batched(new_texts, "NEW")
|
||||
|
||||
# Embed old
|
||||
print("Embedding OLD (existing) chunks...")
|
||||
old_embs, old_tokens, old_time = embed_batched(old_texts, "OLD")
|
||||
|
||||
# Embed queries
|
||||
print(f"Embedding {len(QUERIES)} queries...")
|
||||
q_result = client.embed(QUERIES, model=MODEL, input_type="query")
|
||||
q_embs = q_result.embeddings
|
||||
|
||||
# Search and compare
|
||||
print(f"\n{'=' * 70}")
|
||||
print("PART 3: SEARCH QUALITY COMPARISON")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
for qi, query in enumerate(QUERIES):
|
||||
# Score against new
|
||||
new_scores = [(cosine_sim(q_embs[qi], e), all_new_chunks[i][0], all_new_chunks[i][2][:60]) for i, e in enumerate(new_embs)]
|
||||
new_scores.sort(reverse=True)
|
||||
|
||||
# Score against old
|
||||
old_scores = [(cosine_sim(q_embs[qi], e), all_old_chunks[i][0], all_old_chunks[i][2][:60]) for i, e in enumerate(old_embs)]
|
||||
old_scores.sort(reverse=True)
|
||||
|
||||
print(f"\nQ{qi+1}: {query}")
|
||||
print(f" {'NEW top-1':>10}: [{new_scores[0][0]:.4f}] {new_scores[0][1]}")
|
||||
print(f" {'OLD top-1':>10}: [{old_scores[0][0]:.4f}] {old_scores[0][1]}")
|
||||
if new_scores[0][0] > old_scores[0][0]:
|
||||
print(f" >> NEW better by {new_scores[0][0] - old_scores[0][0]:.4f}")
|
||||
else:
|
||||
print(f" >> OLD better by {old_scores[0][0] - new_scores[0][0]:.4f}")
|
||||
|
||||
# Summary
|
||||
new_avg = sum(max(cosine_sim(q_embs[qi], e) for e in new_embs) for qi in range(len(QUERIES))) / len(QUERIES)
|
||||
old_avg = sum(max(cosine_sim(q_embs[qi], e) for e in old_embs) for qi in range(len(QUERIES))) / len(QUERIES)
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print("SUMMARY")
|
||||
print(f"{'=' * 70}")
|
||||
print(f" {'Metric':<30} {'Old (existing)':>15} {'New (Google Vision)':>20}")
|
||||
print(f" {'-' * 65}")
|
||||
print(f" {'Total chunks':<30} {len(old_texts):>15} {len(new_texts):>20}")
|
||||
print(f" {'Total tokens':<30} {old_tokens:>15,} {new_tokens:>20,}")
|
||||
print(f" {'Embed time':<30} {old_time:>14.1f}s {new_time:>19.1f}s")
|
||||
print(f" {'Avg top-1 score':<30} {old_avg:>15.4f} {new_avg:>20.4f}")
|
||||
print(f" {'Score difference':<30} {'':>15} {new_avg - old_avg:>+20.4f}")
|
||||
|
||||
est_cost = (new_tokens + old_tokens) / 1_000_000 * 0.12
|
||||
print(f"\n Embedding cost: ${est_cost:.3f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
75
scripts/bidi_table.py
Normal file
75
scripts/bidi_table.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BiDi-safe box-drawing table renderer for mixed Hebrew/English terminal output.
|
||||
|
||||
Uses Unicode directional marks to prevent the BiDi algorithm from breaking
|
||||
table alignment when Hebrew text is present.
|
||||
|
||||
Usage as module:
|
||||
from scripts.bidi_table import bidi_table
|
||||
print(bidi_table(['Col1', 'Col2'], [['val1', 'ערך2']]))
|
||||
|
||||
Usage from CLI:
|
||||
python3 scripts/bidi_table.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
LRM = "\u200E" # Left-to-Right Mark
|
||||
RLM = "\u200F" # Right-to-Left Mark
|
||||
LRE = "\u202A" # Left-to-Right Embedding
|
||||
PDF = "\u202C" # Pop Directional Formatting
|
||||
|
||||
_HEB_RE = re.compile(r'[\u0590-\u05FF]')
|
||||
|
||||
|
||||
def _has_hebrew(text: str) -> bool:
|
||||
return bool(_HEB_RE.search(text))
|
||||
|
||||
|
||||
def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
|
||||
"""Render a box-drawing table safe for mixed RTL/LTR terminal display."""
|
||||
ncols = len(headers)
|
||||
|
||||
# Calculate column widths (visual length, not counting bidi marks)
|
||||
col_widths = [len(h) for h in headers]
|
||||
for row in rows:
|
||||
for i, cell in enumerate(row[:ncols]):
|
||||
col_widths[i] = max(col_widths[i], len(cell))
|
||||
|
||||
def hline(left: str, mid: str, right: str) -> str:
|
||||
return left + mid.join("─" * (w + 2) for w in col_widths) + right
|
||||
|
||||
def dataline(cells: list[str]) -> str:
|
||||
parts = []
|
||||
for i in range(ncols):
|
||||
cell = cells[i] if i < len(cells) else ""
|
||||
padded = cell + " " * max(0, col_widths[i] - len(cell))
|
||||
# Wrap each cell: LRE forces left-to-right context for the cell,
|
||||
# so box-drawing chars stay in place. PDF closes the embedding.
|
||||
parts.append(LRE + " " + padded + " " + PDF)
|
||||
return LRM + "│" + ("│").join(parts) + "│"
|
||||
|
||||
lines = [hline("┌", "┬", "┐")]
|
||||
lines.append(dataline(headers))
|
||||
lines.append(hline("├", "┼", "┤"))
|
||||
for row in rows:
|
||||
lines.append(dataline(row))
|
||||
lines.append(hline("└", "┴", "┘"))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
table = bidi_table(
|
||||
["File", "Description", "Model", "Step"],
|
||||
[
|
||||
["claims_extractor.py", "חילוץ טענות מכתבי טענות", "Sonnet", "שלב 3 — הבא בתור"],
|
||||
["brainstorm.py", "סיעור מוחות — כיווני נימוק", "Sonnet", "שלב 4"],
|
||||
["block_writer.py", "כתיבת בלוקים של החלטה", "Sonnet/Opus", "שלב 5"],
|
||||
["qa_validator.py", "בדיקת איכות QA", "Sonnet", "שלב 6"],
|
||||
["style_analyzer.py", "ניתוח סגנון דפנה", "Opus", "חד-פעמי"],
|
||||
["learning_loop.py", "למידה מהחלטה סופית", "Sonnet", "סוף תהליך"],
|
||||
],
|
||||
)
|
||||
print(table)
|
||||
126
scripts/compare_extractions.py
Normal file
126
scripts/compare_extractions.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""Compare existing MD files with freshly extracted text from PDFs."""
|
||||
|
||||
import difflib
|
||||
from pathlib import Path
|
||||
|
||||
DOCS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents")
|
||||
EXTRACTED_DIR = DOCS_DIR / "extracted"
|
||||
|
||||
# Map: existing MD -> extracted MD
|
||||
PAIRS = [
|
||||
("2025-08-14-כתב-ערר-קובר.md", "מרק קובר-כתב ערר.md", "Appeal - Kuber"),
|
||||
("2025-09-01-כתב-תשובה-ליבמן-לערר.md", "תשובה לערר מטעם המשיבים.md", "Response - Livman"),
|
||||
("2025-09-02-כתב-תשובה-ועדת-הראל-לערר.md", "תשובת הועדה המרחבית לערר.md", "Response - Committee"),
|
||||
("2025-10-22-כתב-ערר-מטמון.md", "תשובת המשיב-יצחק מטמון.md", "Response - Matmon"),
|
||||
]
|
||||
|
||||
|
||||
def normalize(text: str) -> str:
|
||||
"""Normalize text for comparison."""
|
||||
# Remove markdown formatting, extra whitespace
|
||||
lines = text.strip().split("\n")
|
||||
lines = [l.strip() for l in lines if l.strip()]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def word_overlap(a: str, b: str) -> float:
|
||||
"""Calculate word-level overlap ratio."""
|
||||
words_a = set(a.split())
|
||||
words_b = set(b.split())
|
||||
if not words_a or not words_b:
|
||||
return 0.0
|
||||
intersection = words_a & words_b
|
||||
return len(intersection) / max(len(words_a), len(words_b))
|
||||
|
||||
|
||||
def main():
|
||||
print(f"{'=' * 70}")
|
||||
print("COMPARISON: Existing MD vs Fresh PDF Extraction")
|
||||
print(f"{'=' * 70}\n")
|
||||
|
||||
summary = []
|
||||
|
||||
for existing_name, extracted_name, label in PAIRS:
|
||||
existing_path = DOCS_DIR / existing_name
|
||||
extracted_path = EXTRACTED_DIR / extracted_name
|
||||
|
||||
if not existing_path.exists():
|
||||
print(f"SKIP: {existing_name} not found")
|
||||
continue
|
||||
if not extracted_path.exists():
|
||||
print(f"SKIP: {extracted_name} not found")
|
||||
continue
|
||||
|
||||
existing_text = existing_path.read_text(encoding="utf-8")
|
||||
extracted_text = extracted_path.read_text(encoding="utf-8")
|
||||
|
||||
existing_norm = normalize(existing_text)
|
||||
extracted_norm = normalize(extracted_text)
|
||||
|
||||
# Stats
|
||||
existing_chars = len(existing_text)
|
||||
extracted_chars = len(extracted_text)
|
||||
existing_words = len(existing_text.split())
|
||||
extracted_words = len(extracted_text.split())
|
||||
|
||||
# Similarity
|
||||
overlap = word_overlap(existing_norm, extracted_norm)
|
||||
|
||||
# Sequence matcher ratio (slower but more accurate)
|
||||
# Use first 5000 chars for speed
|
||||
sm = difflib.SequenceMatcher(None, existing_norm[:5000], extracted_norm[:5000])
|
||||
seq_ratio = sm.ratio()
|
||||
|
||||
# Find lines in extracted but not in existing (new content)
|
||||
existing_lines = set(existing_norm.split("\n"))
|
||||
extracted_lines = set(extracted_norm.split("\n"))
|
||||
new_lines = extracted_lines - existing_lines
|
||||
missing_lines = existing_lines - extracted_lines
|
||||
|
||||
print(f"{'=' * 70}")
|
||||
print(f" {label}")
|
||||
print(f" Existing: {existing_name}")
|
||||
print(f" Extracted: {extracted_name}")
|
||||
print(f"{'=' * 70}")
|
||||
print(f" {'Metric':<30} {'Existing MD':>15} {'Fresh PDF':>15} {'Diff':>10}")
|
||||
print(f" {'-' * 70}")
|
||||
print(f" {'Characters':<30} {existing_chars:>15,} {extracted_chars:>15,} {extracted_chars - existing_chars:>+10,}")
|
||||
print(f" {'Words':<30} {existing_words:>15,} {extracted_words:>15,} {extracted_words - existing_words:>+10,}")
|
||||
print(f" {'Lines':<30} {len(existing_lines):>15,} {len(extracted_lines):>15,} {len(extracted_lines) - len(existing_lines):>+10,}")
|
||||
print(f" {'Word overlap':<30} {overlap:>15.1%}")
|
||||
print(f" {'Sequence similarity':<30} {seq_ratio:>15.1%}")
|
||||
print(f" {'Lines only in fresh PDF':<30} {len(new_lines):>15}")
|
||||
print(f" {'Lines only in existing MD':<30} {len(missing_lines):>15}")
|
||||
|
||||
# Show sample differences
|
||||
if new_lines:
|
||||
print(f"\n Sample lines ONLY in fresh extraction (first 3):")
|
||||
for line in sorted(new_lines)[:3]:
|
||||
print(f" + {line[:100]}")
|
||||
if missing_lines:
|
||||
print(f"\n Sample lines ONLY in existing MD (first 3):")
|
||||
for line in sorted(missing_lines)[:3]:
|
||||
print(f" - {line[:100]}")
|
||||
|
||||
print()
|
||||
|
||||
summary.append({
|
||||
"label": label,
|
||||
"existing_words": existing_words,
|
||||
"extracted_words": extracted_words,
|
||||
"word_overlap": overlap,
|
||||
"seq_similarity": seq_ratio,
|
||||
})
|
||||
|
||||
# Summary table
|
||||
print(f"\n{'=' * 70}")
|
||||
print("SUMMARY")
|
||||
print(f"{'=' * 70}")
|
||||
print(f" {'Document':<25} {'Existing':>10} {'Fresh':>10} {'Overlap':>10} {'Similarity':>12}")
|
||||
print(f" {'-' * 67}")
|
||||
for s in summary:
|
||||
print(f" {s['label']:<25} {s['existing_words']:>10,} {s['extracted_words']:>10,} {s['word_overlap']:>10.1%} {s['seq_similarity']:>12.1%}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
289
scripts/decompose-decisions-v2.py
Normal file
289
scripts/decompose-decisions-v2.py
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decompose final decisions into 12-block structure — V2 calibrated on הכט.
|
||||
|
||||
Key insight: DOCX extraction strips header blocks (א-ד). The real content
|
||||
starts at block ה (opening "לפנינו"). We identify blocks by known section
|
||||
headers and line-by-line analysis.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
BLOCK_DEFS = [
|
||||
("block-alef", 1, "כותרת מוסדית", "template-fill"),
|
||||
("block-bet", 2, "הרכב הוועדה", "template-fill"),
|
||||
("block-gimel", 3, "צדדים", "template-fill"),
|
||||
("block-dalet", 4, "כותרת החלטה", "template-fill"),
|
||||
("block-he", 5, "פתיחה", "paraphrase"),
|
||||
("block-vav", 6, "רקע עובדתי", "reproduction"),
|
||||
("block-zayin", 7, "טענות הצדדים", "paraphrase"),
|
||||
("block-chet", 8, "הליכים בפני ועדת הערר", "reproduction"),
|
||||
("block-tet", 9, "תכניות חלות", "guided-synthesis"),
|
||||
("block-yod", 10, "דיון והכרעה", "rhetorical-construction"),
|
||||
("block-yod-alef", 11, "סיכום", "paraphrase"),
|
||||
("block-yod-bet", 12, "חתימות", "template-fill"),
|
||||
]
|
||||
|
||||
|
||||
def find_line(lines: list[str], pattern: str, start: int = 0) -> int:
|
||||
"""Find first line matching pattern (substring or regex). Returns -1 if not found."""
|
||||
pat = re.compile(pattern)
|
||||
for i in range(start, len(lines)):
|
||||
if pat.search(lines[i]):
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def slice_text(lines: list[str], start: int, end: int) -> str:
|
||||
"""Join lines[start:end] into text."""
|
||||
if start < 0 or end <= start:
|
||||
return ""
|
||||
return "\n".join(lines[start:end]).strip()
|
||||
|
||||
|
||||
def count_words(text: str) -> int:
|
||||
return len(text.split()) if text else 0
|
||||
|
||||
|
||||
def decompose(text: str) -> dict[str, str]:
|
||||
"""Parse decision into blocks. Returns {block_id: content}."""
|
||||
lines = text.split("\n")
|
||||
n = len(lines)
|
||||
blocks = {}
|
||||
|
||||
# Find key section headers
|
||||
# Style 1: רישוי — descriptive headers ("תמצית טענות הצדדים", "דיון והכרעה")
|
||||
# Style 2: היטל השבחה — numbered headers ("א. רקע עובדתי", "ו. דיון והכרעה")
|
||||
opening = find_line(lines, r"^לפנינו\s|^בפנינו\s|^בפני\s*ועדת|^בפני\s*בקשה")
|
||||
|
||||
claims = find_line(lines, r"תמצית\s*טענות|טענות\s*הצדדים|טענות\s*העוררי")
|
||||
if claims == -1:
|
||||
claims = find_line(lines, r"^טענות\s*העוררי")
|
||||
if claims == -1:
|
||||
# היטל השבחה style: "ב. טענות העורר"
|
||||
claims = find_line(lines, r"^[א-ת][\.\)]\s*טענות")
|
||||
|
||||
background = find_line(lines, r"^[א-ת][\.\)]\s*רקע\s*עובדתי")
|
||||
|
||||
proceedings = find_line(lines, r"ההליכים\s*בפני|הליכים\s*בפני|הדיון\s*בפני\s*ועדת\s*הערר")
|
||||
if proceedings == -1:
|
||||
# היטל השבחה: "ד. הבהרות השמאית" or similar procedural sections
|
||||
proceedings = find_line(lines, r"^[א-ת][\.\)]\s*הבהרות|^[א-ת][\.\)]\s*ההליך")
|
||||
|
||||
plans = find_line(lines, r"תכניות\s*חלות|המסגרת\s*הנורמטיבית|הוראות\s*התכנית")
|
||||
if plans == -1:
|
||||
plans = find_line(lines, r"^[א-ת][\.\)]\s*המסגרת\s*הנורמטיבית")
|
||||
|
||||
discussion = find_line(lines, r"^דיון\s*והכרעה|^דיון$|^הכרעה$")
|
||||
if discussion == -1:
|
||||
discussion = find_line(lines, r"^[א-ת][\.\)]\s*דיון\s*והכרעה")
|
||||
|
||||
summary = find_line(lines, r"^סיכום\s*$|^סוף\s*דבר\s*$")
|
||||
if summary == -1:
|
||||
summary = find_line(lines, r"^[א-ת][\.\)]\s*סיכום")
|
||||
signature = find_line(lines, r"^ניתנה?\s*(היום|פה\s*אחד|ביום)")
|
||||
|
||||
# If no explicit discussion header, look for the opening formula
|
||||
if discussion == -1:
|
||||
discussion = find_line(lines, r"לאחר\s*שבחנו\s*את\s*טענות")
|
||||
|
||||
# ── Header blocks (א-ד): everything before opening ──
|
||||
if opening >= 0:
|
||||
header_text = slice_text(lines, 0, opening)
|
||||
if header_text:
|
||||
# Try to split header, but usually DOCX extraction loses these
|
||||
blocks["block-alef"] = header_text
|
||||
else:
|
||||
blocks["block-alef"] = ""
|
||||
else:
|
||||
blocks["block-alef"] = ""
|
||||
|
||||
blocks["block-bet"] = "" # Usually lost in extraction
|
||||
blocks["block-gimel"] = ""
|
||||
blocks["block-dalet"] = "החלטה"
|
||||
|
||||
# ── Block ה: Opening — first 1-3 paragraphs from "לפנינו" ──
|
||||
if opening >= 0:
|
||||
next_section = claims if claims > opening else discussion if discussion > opening else n
|
||||
opening_end = opening + 1
|
||||
for i in range(opening + 1, min(opening + 5, next_section)):
|
||||
line = lines[i].strip()
|
||||
if not line:
|
||||
break
|
||||
opening_end = i + 1
|
||||
blocks["block-he"] = slice_text(lines, opening, opening_end)
|
||||
else:
|
||||
blocks["block-he"] = ""
|
||||
|
||||
# ── Block ו: Background ──
|
||||
# Style 1 (רישוי): after opening, before claims
|
||||
# Style 2 (היטל השבחה): explicit "א. רקע עובדתי" section
|
||||
if background >= 0:
|
||||
# Explicit background header (היטל השבחה style)
|
||||
bg_end = claims if claims > background else (proceedings if proceedings > background else (discussion if discussion > background else n))
|
||||
blocks["block-vav"] = slice_text(lines, background, bg_end)
|
||||
# In this case, opening (ה) might not exist — "לפנינו" may be absent
|
||||
elif opening >= 0 and claims > opening:
|
||||
bg_start = opening + 1
|
||||
he_lines = count_words(blocks.get("block-he", ""))
|
||||
if he_lines > 0:
|
||||
he_end = opening
|
||||
for i in range(opening, min(opening + 5, claims)):
|
||||
if lines[i].strip():
|
||||
he_end = i + 1
|
||||
else:
|
||||
break
|
||||
bg_start = he_end
|
||||
blocks["block-vav"] = slice_text(lines, bg_start, claims)
|
||||
elif opening >= 0 and discussion > opening:
|
||||
blocks["block-vav"] = slice_text(lines, opening + 1, discussion)
|
||||
else:
|
||||
blocks["block-vav"] = ""
|
||||
|
||||
# ── Block ז: Claims — from claims header to next section ──
|
||||
if claims >= 0:
|
||||
claims_end = min(
|
||||
x for x in [proceedings, plans, discussion, summary, n]
|
||||
if x > claims
|
||||
)
|
||||
blocks["block-zayin"] = slice_text(lines, claims, claims_end)
|
||||
else:
|
||||
blocks["block-zayin"] = ""
|
||||
|
||||
# ── Block ח: Proceedings (optional) ──
|
||||
if proceedings >= 0:
|
||||
proc_end = min(
|
||||
x for x in [plans, discussion, summary, n]
|
||||
if x > proceedings
|
||||
)
|
||||
blocks["block-chet"] = slice_text(lines, proceedings, proc_end)
|
||||
else:
|
||||
blocks["block-chet"] = ""
|
||||
|
||||
# ── Block ט: Plans (optional) ──
|
||||
if plans >= 0 and (discussion == -1 or plans < discussion):
|
||||
plans_end = min(
|
||||
x for x in [discussion, summary, n]
|
||||
if x > plans
|
||||
)
|
||||
blocks["block-tet"] = slice_text(lines, plans, plans_end)
|
||||
else:
|
||||
blocks["block-tet"] = ""
|
||||
|
||||
# ── Block י: Discussion ──
|
||||
if discussion >= 0:
|
||||
disc_end = summary if summary > discussion else (signature if signature > discussion else n)
|
||||
blocks["block-yod"] = slice_text(lines, discussion, disc_end)
|
||||
else:
|
||||
blocks["block-yod"] = ""
|
||||
|
||||
# ── Block יא: Summary ──
|
||||
if summary >= 0:
|
||||
summ_end = signature if signature > summary else n
|
||||
blocks["block-yod-alef"] = slice_text(lines, summary, summ_end)
|
||||
else:
|
||||
blocks["block-yod-alef"] = ""
|
||||
|
||||
# ── Block יב: Signatures ──
|
||||
if signature >= 0:
|
||||
blocks["block-yod-bet"] = slice_text(lines, signature, n)
|
||||
else:
|
||||
blocks["block-yod-bet"] = ""
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
decisions = await conn.fetch(
|
||||
"""SELECT d.id as decision_id, c.case_number, c.title,
|
||||
doc.extracted_text
|
||||
FROM decisions d
|
||||
JOIN cases c ON c.id = d.case_id
|
||||
JOIN documents doc ON doc.case_id = d.case_id AND doc.doc_type = 'decision'
|
||||
WHERE d.status = 'final'
|
||||
ORDER BY c.case_number"""
|
||||
)
|
||||
|
||||
for dec in decisions:
|
||||
decision_id = dec["decision_id"]
|
||||
case_number = dec["case_number"]
|
||||
text = dec["extracted_text"]
|
||||
total_words = count_words(text)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"מפרק: {case_number} — {dec['title']}")
|
||||
print(f"סה\"כ מילים: {total_words}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
parsed = decompose(text)
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Delete existing blocks
|
||||
await conn.execute(
|
||||
"DELETE FROM decision_blocks WHERE decision_id = $1", decision_id
|
||||
)
|
||||
|
||||
total_parsed_words = 0
|
||||
for block_id, block_index, title, gen_type in BLOCK_DEFS:
|
||||
content = parsed.get(block_id, "")
|
||||
wc = count_words(content)
|
||||
weight = round(wc / total_words * 100, 1) if total_words > 0 and wc > 0 else 0
|
||||
status = "final" if wc > 0 else "empty"
|
||||
total_parsed_words += wc
|
||||
|
||||
await conn.execute(
|
||||
"""INSERT INTO decision_blocks
|
||||
(decision_id, block_id, block_index, title, content,
|
||||
word_count, weight_percent, generation_type, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
|
||||
decision_id, block_id, block_index, title,
|
||||
content, wc, weight, gen_type, status,
|
||||
)
|
||||
|
||||
marker = "✅" if wc > 0 else "⬜"
|
||||
print(f" {marker} {block_id:18s} | {title:25s} | {wc:5d} מילים | {weight:5.1f}%")
|
||||
|
||||
# Update decision totals
|
||||
disc_words = count_words(parsed.get("block-yod", ""))
|
||||
disc_paras = len([p for p in parsed.get("block-yod", "").split("\n") if p.strip() and len(p.strip()) > 20])
|
||||
await conn.execute(
|
||||
"UPDATE decisions SET total_words = $1, total_paragraphs = $2, updated_at = now() WHERE id = $3",
|
||||
total_words, disc_paras, decision_id,
|
||||
)
|
||||
|
||||
coverage = round(total_parsed_words / total_words * 100, 1) if total_words > 0 else 0
|
||||
print(f" --- כיסוי: {total_parsed_words}/{total_words} מילים ({coverage}%)")
|
||||
|
||||
# Summary
|
||||
async with pool.acquire() as conn:
|
||||
stats = await conn.fetch(
|
||||
"""SELECT block_id, count(*) as decisions,
|
||||
avg(word_count) as avg_words,
|
||||
avg(weight_percent) as avg_weight
|
||||
FROM decision_blocks
|
||||
WHERE word_count > 0
|
||||
GROUP BY block_id ORDER BY block_id"""
|
||||
)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("סטטיסטיקה לפי בלוק (רק בלוקים עם תוכן):")
|
||||
for s in stats:
|
||||
print(f" {s['block_id']:18s} | {s['decisions']} החלטות | ממוצע {s['avg_words']:.0f} מילים | {s['avg_weight']:.1f}%")
|
||||
|
||||
await close_pool()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
323
scripts/decompose-decisions.py
Normal file
323
scripts/decompose-decisions.py
Normal file
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decompose 6 final decisions into 12-block structure.
|
||||
|
||||
Uses heuristic parsing based on known section headers in Dafna's decisions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Block definitions with detection patterns
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
BLOCKS = [
|
||||
{
|
||||
"block_id": "block-alef",
|
||||
"block_index": 1,
|
||||
"title": "כותרת מוסדית",
|
||||
"generation_type": "template-fill",
|
||||
},
|
||||
{
|
||||
"block_id": "block-bet",
|
||||
"block_index": 2,
|
||||
"title": "הרכב הוועדה",
|
||||
"generation_type": "template-fill",
|
||||
},
|
||||
{
|
||||
"block_id": "block-gimel",
|
||||
"block_index": 3,
|
||||
"title": "צדדים",
|
||||
"generation_type": "template-fill",
|
||||
},
|
||||
{
|
||||
"block_id": "block-dalet",
|
||||
"block_index": 4,
|
||||
"title": "כותרת החלטה",
|
||||
"generation_type": "template-fill",
|
||||
},
|
||||
{
|
||||
"block_id": "block-he",
|
||||
"block_index": 5,
|
||||
"title": "פתיחה",
|
||||
"generation_type": "paraphrase",
|
||||
},
|
||||
{
|
||||
"block_id": "block-vav",
|
||||
"block_index": 6,
|
||||
"title": "רקע עובדתי",
|
||||
"generation_type": "reproduction",
|
||||
},
|
||||
{
|
||||
"block_id": "block-zayin",
|
||||
"block_index": 7,
|
||||
"title": "טענות הצדדים",
|
||||
"generation_type": "paraphrase",
|
||||
},
|
||||
{
|
||||
"block_id": "block-chet",
|
||||
"block_index": 8,
|
||||
"title": "הליכים בפני ועדת הערר",
|
||||
"generation_type": "reproduction",
|
||||
},
|
||||
{
|
||||
"block_id": "block-tet",
|
||||
"block_index": 9,
|
||||
"title": "תכניות חלות",
|
||||
"generation_type": "guided-synthesis",
|
||||
},
|
||||
{
|
||||
"block_id": "block-yod",
|
||||
"block_index": 10,
|
||||
"title": "דיון והכרעה",
|
||||
"generation_type": "rhetorical-construction",
|
||||
},
|
||||
{
|
||||
"block_id": "block-yod-alef",
|
||||
"block_index": 11,
|
||||
"title": "סיכום",
|
||||
"generation_type": "paraphrase",
|
||||
},
|
||||
{
|
||||
"block_id": "block-yod-bet",
|
||||
"block_index": 12,
|
||||
"title": "חתימות",
|
||||
"generation_type": "template-fill",
|
||||
},
|
||||
]
|
||||
|
||||
# Section header patterns (Hebrew)
|
||||
SECTION_PATTERNS = {
|
||||
"claims": re.compile(r"תמצית\s*טענות\s*הצדדים|טענות\s*הצדדים|טענות\s*העוררי"),
|
||||
"proceedings": re.compile(r"ההליכים\s*בפני\s*ועדת\s*הערר|הליכים\s*בפני\s*הוועדה|הדיון\s*בפני\s*ועדת\s*הערר"),
|
||||
"plans": re.compile(r"תכניות\s*חלות|המסגרת\s*התכנונית|הוראות\s*התכנית"),
|
||||
"discussion": re.compile(r"דיון\s*והכרעה|דיון|הכרעה"),
|
||||
"summary": re.compile(r"^סיכום$|^סוף\s*דבר$", re.MULTILINE),
|
||||
"appellant_claims": re.compile(r"טענות\s*העוררי|טענות\s*העורר"),
|
||||
"respondent_claims": re.compile(r"עמדת\s*הוועדה\s*המקומית|תגובת\s*המשיבה|עמדת\s*המשיב"),
|
||||
"permit_applicant": re.compile(r"עמדת\s*מבקש|עמדת\s*מגיש|עמדת\s*היזם"),
|
||||
"panel": re.compile(r"בפני[:\s]|יו\"ר"),
|
||||
"parties_vs": re.compile(r"\s*נגד\s*"),
|
||||
"decision_title": re.compile(r"^החלטה$", re.MULTILINE),
|
||||
"opening": re.compile(r"^לפנינו\s|^בפנינו\s"),
|
||||
"signature": re.compile(r"ניתנה?\s*(היום|פה\s*אחד|ביום)|חתימ"),
|
||||
}
|
||||
|
||||
|
||||
def find_section_start(text: str, pattern: re.Pattern) -> int:
|
||||
"""Find the character position where a section starts."""
|
||||
match = pattern.search(text)
|
||||
return match.start() if match else -1
|
||||
|
||||
|
||||
def decompose_decision(text: str) -> list[dict]:
|
||||
"""Parse decision text into blocks based on section headers."""
|
||||
lines = text.split("\n")
|
||||
total_len = len(text)
|
||||
|
||||
# Find key section boundaries
|
||||
pos_claims = find_section_start(text, SECTION_PATTERNS["claims"])
|
||||
pos_proceedings = find_section_start(text, SECTION_PATTERNS["proceedings"])
|
||||
pos_plans = find_section_start(text, SECTION_PATTERNS["plans"])
|
||||
pos_discussion = find_section_start(text, SECTION_PATTERNS["discussion"])
|
||||
pos_summary = find_section_start(text, SECTION_PATTERNS["summary"])
|
||||
pos_signature = find_section_start(text, SECTION_PATTERNS["signature"])
|
||||
pos_opening = find_section_start(text, SECTION_PATTERNS["opening"])
|
||||
pos_decision_title = find_section_start(text, SECTION_PATTERNS["decision_title"])
|
||||
pos_panel = find_section_start(text, SECTION_PATTERNS["panel"])
|
||||
pos_parties = find_section_start(text, SECTION_PATTERNS["parties_vs"])
|
||||
|
||||
# Build blocks based on what we found
|
||||
blocks = []
|
||||
|
||||
# Blocks א-ד: Header area (before the opening "לפנינו")
|
||||
header_end = pos_opening if pos_opening > 0 else pos_claims if pos_claims > 0 else 500
|
||||
header_text = text[:header_end].strip()
|
||||
|
||||
# Try to split header into institutional header, panel, parties, title
|
||||
if pos_panel > 0 and pos_panel < header_end:
|
||||
blocks.append({"block_id": "block-alef", "content": text[:pos_panel].strip()})
|
||||
|
||||
if pos_parties > 0 and pos_parties < header_end:
|
||||
blocks.append({"block_id": "block-bet", "content": text[pos_panel:pos_parties].strip()})
|
||||
if pos_decision_title > 0 and pos_decision_title < header_end:
|
||||
blocks.append({"block_id": "block-gimel", "content": text[pos_parties:pos_decision_title].strip()})
|
||||
blocks.append({"block_id": "block-dalet", "content": "החלטה"})
|
||||
else:
|
||||
blocks.append({"block_id": "block-gimel", "content": text[pos_parties:header_end].strip()})
|
||||
blocks.append({"block_id": "block-dalet", "content": "החלטה"})
|
||||
else:
|
||||
blocks.append({"block_id": "block-bet", "content": text[pos_panel:header_end].strip()})
|
||||
blocks.append({"block_id": "block-gimel", "content": ""})
|
||||
blocks.append({"block_id": "block-dalet", "content": "החלטה"})
|
||||
else:
|
||||
# Can't split — put everything in alef
|
||||
blocks.append({"block_id": "block-alef", "content": header_text})
|
||||
blocks.append({"block_id": "block-bet", "content": ""})
|
||||
blocks.append({"block_id": "block-gimel", "content": ""})
|
||||
blocks.append({"block_id": "block-dalet", "content": "החלטה"})
|
||||
|
||||
# Block ה: Opening — from "לפנינו" to claims section
|
||||
if pos_opening > 0:
|
||||
opening_end = pos_claims if pos_claims > pos_opening else pos_discussion if pos_discussion > pos_opening else total_len
|
||||
# Opening is usually just 1-3 paragraphs
|
||||
opening_text = text[pos_opening:min(pos_opening + 1000, opening_end)].strip()
|
||||
# Find end of first few paragraphs
|
||||
para_breaks = [i for i, c in enumerate(opening_text) if c == '\n' and i > 50]
|
||||
if len(para_breaks) >= 2:
|
||||
opening_text = opening_text[:para_breaks[1]].strip()
|
||||
blocks.append({"block_id": "block-he", "content": opening_text})
|
||||
|
||||
# Block ו: Background — from after opening to claims
|
||||
if pos_claims > pos_opening:
|
||||
bg_start = pos_opening + len(opening_text)
|
||||
blocks.append({"block_id": "block-vav", "content": text[bg_start:pos_claims].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-vav", "content": ""})
|
||||
else:
|
||||
blocks.append({"block_id": "block-he", "content": ""})
|
||||
blocks.append({"block_id": "block-vav", "content": ""})
|
||||
|
||||
# Block ז: Claims
|
||||
if pos_claims > 0:
|
||||
claims_end = pos_proceedings if pos_proceedings > pos_claims else pos_discussion if pos_discussion > pos_claims else pos_summary if pos_summary > pos_claims else total_len
|
||||
blocks.append({"block_id": "block-zayin", "content": text[pos_claims:claims_end].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-zayin", "content": ""})
|
||||
|
||||
# Block ח: Proceedings (optional)
|
||||
if pos_proceedings > 0:
|
||||
proc_end = pos_plans if pos_plans > pos_proceedings else pos_discussion if pos_discussion > pos_proceedings else pos_summary if pos_summary > pos_proceedings else total_len
|
||||
blocks.append({"block_id": "block-chet", "content": text[pos_proceedings:proc_end].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-chet", "content": ""})
|
||||
|
||||
# Block ט: Plans (optional)
|
||||
if pos_plans > 0 and pos_plans < (pos_discussion if pos_discussion > 0 else total_len):
|
||||
plans_end = pos_discussion if pos_discussion > pos_plans else pos_summary if pos_summary > pos_plans else total_len
|
||||
blocks.append({"block_id": "block-tet", "content": text[pos_plans:plans_end].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-tet", "content": ""})
|
||||
|
||||
# Block י: Discussion
|
||||
if pos_discussion > 0:
|
||||
disc_end = pos_summary if pos_summary > pos_discussion else pos_signature if pos_signature > pos_discussion else total_len
|
||||
blocks.append({"block_id": "block-yod", "content": text[pos_discussion:disc_end].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-yod", "content": ""})
|
||||
|
||||
# Block יא: Summary
|
||||
if pos_summary > 0:
|
||||
summ_end = pos_signature if pos_signature > pos_summary else total_len
|
||||
blocks.append({"block_id": "block-yod-alef", "content": text[pos_summary:summ_end].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-yod-alef", "content": ""})
|
||||
|
||||
# Block יב: Signatures
|
||||
if pos_signature > 0:
|
||||
blocks.append({"block_id": "block-yod-bet", "content": text[pos_signature:].strip()})
|
||||
else:
|
||||
blocks.append({"block_id": "block-yod-bet", "content": ""})
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
decisions = await conn.fetch(
|
||||
"""SELECT d.id as decision_id, c.case_number, c.title, d.total_words,
|
||||
doc.extracted_text
|
||||
FROM decisions d
|
||||
JOIN cases c ON c.id = d.case_id
|
||||
JOIN documents doc ON doc.case_id = d.case_id AND doc.doc_type = 'decision'
|
||||
WHERE d.status = 'final'
|
||||
ORDER BY c.case_number"""
|
||||
)
|
||||
|
||||
for dec in decisions:
|
||||
decision_id = dec["decision_id"]
|
||||
case_number = dec["case_number"]
|
||||
text = dec["extracted_text"]
|
||||
total_words = len(text.split())
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"מפרק: {case_number} — {dec['title']}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Decompose
|
||||
blocks = decompose_decision(text)
|
||||
|
||||
# Merge with block metadata
|
||||
block_data = []
|
||||
for block_def in BLOCKS:
|
||||
matching = [b for b in blocks if b["block_id"] == block_def["block_id"]]
|
||||
content = matching[0]["content"] if matching else ""
|
||||
word_count = len(content.split()) if content else 0
|
||||
weight = round((word_count / total_words * 100), 2) if total_words > 0 and word_count > 0 else 0
|
||||
|
||||
block_data.append({
|
||||
**block_def,
|
||||
"content": content,
|
||||
"word_count": word_count,
|
||||
"weight_percent": weight,
|
||||
"status": "final" if content else "empty",
|
||||
})
|
||||
|
||||
# Print summary
|
||||
for b in block_data:
|
||||
status = "✅" if b["word_count"] > 0 else "⬜"
|
||||
print(f" {status} {b['block_id']:18s} | {b['title']:25s} | {b['word_count']:5d} מילים | {b['weight_percent']:5.1f}%")
|
||||
|
||||
# Store in DB
|
||||
async with pool.acquire() as conn:
|
||||
# Delete existing blocks for this decision
|
||||
await conn.execute(
|
||||
"DELETE FROM decision_blocks WHERE decision_id = $1", decision_id
|
||||
)
|
||||
|
||||
for b in block_data:
|
||||
await conn.execute(
|
||||
"""INSERT INTO decision_blocks
|
||||
(decision_id, block_id, block_index, title, content,
|
||||
word_count, weight_percent, generation_type, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
|
||||
decision_id,
|
||||
b["block_id"], b["block_index"], b["title"],
|
||||
b["content"], b["word_count"], b["weight_percent"],
|
||||
b["generation_type"], b["status"],
|
||||
)
|
||||
|
||||
# Count paragraphs in discussion block
|
||||
discussion = [b for b in block_data if b["block_id"] == "block-yod"][0]
|
||||
if discussion["content"]:
|
||||
paragraphs = [p.strip() for p in discussion["content"].split("\n") if p.strip() and len(p.strip()) > 20]
|
||||
await conn.execute(
|
||||
"UPDATE decisions SET total_paragraphs = $1 WHERE id = $2",
|
||||
len(paragraphs), decision_id,
|
||||
)
|
||||
|
||||
# Final summary
|
||||
async with pool.acquire() as conn:
|
||||
block_count = await conn.fetchval("SELECT count(*) FROM decision_blocks")
|
||||
non_empty = await conn.fetchval("SELECT count(*) FROM decision_blocks WHERE status = 'final'")
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"✅ סה\"כ בלוקים: {block_count} ({non_empty} עם תוכן)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
139
scripts/export-decision-docx.py
Normal file
139
scripts/export-decision-docx.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export a decision from DB to DOCX using the CJS template generator.
|
||||
|
||||
Usage: python export-decision-docx.py <case_number> [output.docx]
|
||||
|
||||
Pulls decision blocks from DB, generates structure JSON,
|
||||
invokes create-decision-structure.cjs to produce DOCX.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
CJS_SCRIPT = Path(__file__).parent.parent / "skills" / "decision" / "scripts" / "create-decision-structure.cjs"
|
||||
|
||||
|
||||
def block_id_to_hebrew(block_id: str) -> str:
|
||||
"""Map block_id to Hebrew letter label."""
|
||||
mapping = {
|
||||
"block-alef": "א", "block-bet": "ב", "block-gimel": "ג",
|
||||
"block-dalet": "ד", "block-he": "ה", "block-vav": "ו",
|
||||
"block-zayin": "ז", "block-chet": "ח", "block-tet": "ט",
|
||||
"block-yod": "י", "block-yod-alef": "יא", "block-yod-bet": "יב",
|
||||
}
|
||||
return mapping.get(block_id, "")
|
||||
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("שימוש: python export-decision-docx.py <מספר_תיק> [output.docx]")
|
||||
sys.exit(1)
|
||||
|
||||
case_number = sys.argv[1]
|
||||
output_path = sys.argv[2] if len(sys.argv) > 2 else f"החלטה-{case_number}.docx"
|
||||
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get case info
|
||||
case = await conn.fetchrow(
|
||||
"SELECT * FROM cases WHERE case_number = $1", case_number
|
||||
)
|
||||
if not case:
|
||||
print(f"תיק {case_number} לא נמצא")
|
||||
sys.exit(1)
|
||||
|
||||
# Get decision
|
||||
decision = await conn.fetchrow(
|
||||
"SELECT * FROM decisions WHERE case_id = $1 AND status = 'final'",
|
||||
case["id"],
|
||||
)
|
||||
if not decision:
|
||||
print(f"אין החלטה סופית לתיק {case_number}")
|
||||
sys.exit(1)
|
||||
|
||||
# Get blocks
|
||||
blocks = await conn.fetch(
|
||||
"""SELECT block_id, block_index, title, content, word_count
|
||||
FROM decision_blocks
|
||||
WHERE decision_id = $1
|
||||
ORDER BY block_index""",
|
||||
decision["id"],
|
||||
)
|
||||
|
||||
await close_pool()
|
||||
|
||||
# Build structure JSON for CJS script
|
||||
appellants = json.loads(case["appellants"]) if isinstance(case["appellants"], str) else case["appellants"]
|
||||
respondents = json.loads(case["respondents"]) if isinstance(case["respondents"], str) else case["respondents"]
|
||||
|
||||
structure = {
|
||||
"metadata": {
|
||||
"case_number": case["case_number"],
|
||||
"title": case["title"],
|
||||
"subject": case["subject"],
|
||||
"property_address": case["property_address"],
|
||||
"committee": case["committee_type"],
|
||||
"outcome": decision["outcome"] or "",
|
||||
"decision_date": str(decision["decision_date"]) if decision["decision_date"] else "",
|
||||
"author": decision["author"],
|
||||
},
|
||||
"parties": {
|
||||
"appellants": [{"name": a} for a in appellants],
|
||||
"respondents": [{"name": r} for r in respondents],
|
||||
},
|
||||
"blocks": [],
|
||||
}
|
||||
|
||||
for block in blocks:
|
||||
content = block["content"] or ""
|
||||
# Skip empty header blocks
|
||||
if block["block_id"] in ("block-alef", "block-bet", "block-gimel", "block-dalet") and not content:
|
||||
continue
|
||||
|
||||
paragraphs = [p.strip() for p in content.split("\n") if p.strip()]
|
||||
|
||||
structure["blocks"].append({
|
||||
"id": block["block_id"],
|
||||
"index": block["block_index"],
|
||||
"title": block["title"],
|
||||
"hebrew_letter": block_id_to_hebrew(block["block_id"]),
|
||||
"word_count": block["word_count"],
|
||||
"paragraphs": paragraphs,
|
||||
})
|
||||
|
||||
# Write JSON (absolute paths)
|
||||
output_abs = Path(output_path).resolve()
|
||||
json_path = output_abs.with_suffix(".json")
|
||||
json_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(structure, f, ensure_ascii=False, indent=2)
|
||||
print(f"JSON נוצר: {json_path}")
|
||||
|
||||
# Run CJS script with absolute paths
|
||||
result = subprocess.run(
|
||||
["node", str(CJS_SCRIPT), str(json_path), str(output_abs)],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(CJS_SCRIPT.parent),
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ DOCX נוצר: {output_path}")
|
||||
else:
|
||||
print(f"❌ שגיאה ביצירת DOCX:")
|
||||
print(result.stderr)
|
||||
# JSON is still available for manual processing
|
||||
print(f"ה-JSON זמין: {json_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
134
scripts/extract-citations.py
Normal file
134
scripts/extract-citations.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract case law citations from block-yod and link to case_law table."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
# Patterns for Israeli case law citations
|
||||
CITATION_PATTERNS = [
|
||||
# עע"מ, בג"ץ, ע"א, etc.
|
||||
re.compile(r'(עע"מ|בג"ץ|ע"א|בר"ם|עת"מ|עמ"נ|ע"ע|רע"א|דנ"א|בש"א)\s*(\d[\d/\-]+)'),
|
||||
# ערר with number
|
||||
re.compile(r'ערר\s*\(?\s*(?:מרכז|ירושלים|חי\'?|ת"א|דרום|צפון)?\s*\)?\s*(\d[\d/\-]+)'),
|
||||
# ערר without district
|
||||
re.compile(r'ערר\s+(\d{3,5}[\-/]\d{2,4})'),
|
||||
]
|
||||
|
||||
|
||||
def extract_citations_from_text(text: str) -> list[dict]:
|
||||
"""Find all case law citations in text."""
|
||||
citations = []
|
||||
seen = set()
|
||||
|
||||
for pattern in CITATION_PATTERNS:
|
||||
for match in pattern.finditer(text):
|
||||
full_match = match.group(0)
|
||||
if full_match in seen:
|
||||
continue
|
||||
seen.add(full_match)
|
||||
|
||||
# Get surrounding context (50 chars before and after)
|
||||
start = max(0, match.start() - 50)
|
||||
end = min(len(text), match.end() + 100)
|
||||
context = text[start:end].replace("\n", " ")
|
||||
|
||||
citations.append({
|
||||
"citation_text": full_match,
|
||||
"context": context,
|
||||
})
|
||||
|
||||
return citations
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get all block-yod content with decision info
|
||||
blocks = await conn.fetch(
|
||||
"""SELECT db.content, d.id as decision_id, c.case_number
|
||||
FROM decision_blocks db
|
||||
JOIN decisions d ON d.id = db.decision_id
|
||||
JOIN cases c ON c.id = d.case_id
|
||||
WHERE db.block_id = 'block-yod' AND db.word_count > 0
|
||||
ORDER BY c.case_number"""
|
||||
)
|
||||
|
||||
# Get existing case_law for matching
|
||||
case_laws = await conn.fetch("SELECT id, case_number, case_name FROM case_law")
|
||||
case_law_map = {}
|
||||
for cl in case_laws:
|
||||
# Index by various forms of the case number
|
||||
case_law_map[cl["case_number"]] = cl["id"]
|
||||
# Also index by short number (e.g., "3975/22" from "עע"מ 3975/22")
|
||||
parts = cl["case_number"].split()
|
||||
if len(parts) > 1:
|
||||
case_law_map[parts[-1]] = cl["id"]
|
||||
|
||||
total_citations = 0
|
||||
total_linked = 0
|
||||
|
||||
for block in blocks:
|
||||
case_number = block["case_number"]
|
||||
decision_id = block["decision_id"]
|
||||
text = block["content"]
|
||||
|
||||
citations = extract_citations_from_text(text)
|
||||
|
||||
if not citations:
|
||||
continue
|
||||
|
||||
print(f"\n{case_number}: {len(citations)} ציטוטים נמצאו")
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
for cit in citations:
|
||||
total_citations += 1
|
||||
|
||||
# Try to match to case_law table
|
||||
case_law_id = None
|
||||
for key, cl_id in case_law_map.items():
|
||||
if key in cit["citation_text"] or cit["citation_text"] in key:
|
||||
case_law_id = cl_id
|
||||
break
|
||||
|
||||
if case_law_id:
|
||||
# Check if already exists
|
||||
existing = await conn.fetchval(
|
||||
"""SELECT id FROM case_law_citations
|
||||
WHERE case_law_id = $1 AND decision_id = $2""",
|
||||
case_law_id, decision_id,
|
||||
)
|
||||
if not existing:
|
||||
await conn.execute(
|
||||
"""INSERT INTO case_law_citations
|
||||
(case_law_id, decision_id, citation_type, context_text)
|
||||
VALUES ($1, $2, 'support', $3)""",
|
||||
case_law_id, decision_id, cit["context"],
|
||||
)
|
||||
total_linked += 1
|
||||
print(f" ✅ {cit['citation_text'][:40]} → קושר לפסיקה")
|
||||
else:
|
||||
print(f" ⬜ {cit['citation_text'][:40]} — לא נמצא ב-DB")
|
||||
|
||||
# Summary
|
||||
async with pool.acquire() as conn:
|
||||
total_in_db = await conn.fetchval("SELECT count(*) FROM case_law_citations")
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"סה\"כ ציטוטים שנמצאו: {total_citations}")
|
||||
print(f"סה\"כ קושרו לפסיקה ב-DB: {total_linked}")
|
||||
print(f"סה\"כ ב-case_law_citations: {total_in_db}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
228
scripts/extract-claims.py
Normal file
228
scripts/extract-claims.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract individual claims from block-zayin of each decision.
|
||||
|
||||
Identifies party sub-sections and individual claims (paragraphs).
|
||||
Stores in the claims table with party_role classification.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
# Party role detection patterns
|
||||
PARTY_PATTERNS = [
|
||||
# Appellants
|
||||
(r"טענות\s*העוררי[םן]|טענות\s*העורר\b|טענות\s*המבקש|טענות\s*המערער", "appellant"),
|
||||
# Respondent - local committee
|
||||
(r"עמדת\s*הוועדה\s*המקומית|עמדת\s*המשיבה|טענות\s*המשיבה|תגובת\s*המשיבה|הוועדה\s*המקומית$", "committee"),
|
||||
# Respondent - general
|
||||
(r"עמדת\s*המשיבי[םן]|עמדת\s*המשיב\b|טענות\s*המשיבי[םן]|טענות\s*המשיב\b", "respondent"),
|
||||
# Permit applicant
|
||||
(r"מבקשי\s*ההיתר|עמדת\s*מבקש|עמדת\s*היזם|מגישי\s*התכנית", "permit_applicant"),
|
||||
# Appraiser clarifications (היטל השבחה)
|
||||
(r"הבהרות\s*השמא|התייחסות\s*הצדדים", "appraiser"),
|
||||
]
|
||||
|
||||
|
||||
def detect_party_role(line: str) -> str | None:
|
||||
"""Detect if a line is a party section header. Returns role or None."""
|
||||
for pattern, role in PARTY_PATTERNS:
|
||||
if re.search(pattern, line):
|
||||
return role
|
||||
return None
|
||||
|
||||
|
||||
def is_section_header(line: str) -> bool:
|
||||
"""Check if line is a section/sub-section header (not a claim)."""
|
||||
line = line.strip()
|
||||
if not line:
|
||||
return False
|
||||
# Very short lines that are headers
|
||||
if len(line) < 50 and (
|
||||
detect_party_role(line) is not None
|
||||
or re.match(r"^תמצית\s*טענות", line)
|
||||
or re.match(r"^[א-ת][\.\)]\s*טענות", line)
|
||||
or re.match(r"^[א-ת][\.\)]\s*כללי", line)
|
||||
or re.match(r"^\d+\.\s*$", line) # just a number
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_numbered_sub_header(line: str) -> bool:
|
||||
"""Check if line is a numbered topic header within claims (e.g., '2. שיעור ההפקעה')."""
|
||||
return bool(re.match(r"^\d+\.\s+\S.{3,40}$", line.strip()))
|
||||
|
||||
|
||||
def extract_claims_from_block(text: str) -> list[dict]:
|
||||
"""Extract individual claims grouped by party from block-zayin text."""
|
||||
lines = text.split("\n")
|
||||
claims = []
|
||||
current_role = "appellant" # default if no header found
|
||||
current_claim_lines = []
|
||||
claim_index = 0
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
# Check for party header — must be a SHORT line (header, not claim content)
|
||||
role = detect_party_role(stripped) if len(stripped.split()) <= 8 else None
|
||||
if role:
|
||||
# Save accumulated claim
|
||||
if current_claim_lines:
|
||||
claim_text = "\n".join(current_claim_lines).strip()
|
||||
if len(claim_text) > 30:
|
||||
claims.append({
|
||||
"party_role": current_role,
|
||||
"claim_text": claim_text,
|
||||
"claim_index": claim_index,
|
||||
})
|
||||
claim_index += 1
|
||||
current_claim_lines = []
|
||||
current_role = role
|
||||
continue
|
||||
|
||||
# Skip generic section headers
|
||||
if is_section_header(stripped):
|
||||
# Save accumulated claim before skipping header
|
||||
if current_claim_lines:
|
||||
claim_text = "\n".join(current_claim_lines).strip()
|
||||
if len(claim_text) > 30:
|
||||
claims.append({
|
||||
"party_role": current_role,
|
||||
"claim_text": claim_text,
|
||||
"claim_index": claim_index,
|
||||
})
|
||||
claim_index += 1
|
||||
current_claim_lines = []
|
||||
continue
|
||||
|
||||
# Numbered sub-header in היטל השבחה style (e.g., "2. שיעור ההפקעה")
|
||||
# starts a new claim
|
||||
if is_numbered_sub_header(stripped):
|
||||
if current_claim_lines:
|
||||
claim_text = "\n".join(current_claim_lines).strip()
|
||||
if len(claim_text) > 30:
|
||||
claims.append({
|
||||
"party_role": current_role,
|
||||
"claim_text": claim_text,
|
||||
"claim_index": claim_index,
|
||||
})
|
||||
claim_index += 1
|
||||
current_claim_lines = [stripped]
|
||||
continue
|
||||
|
||||
# Each substantial paragraph is a separate claim
|
||||
# Save previous accumulated claim first
|
||||
if current_claim_lines:
|
||||
claim_text = "\n".join(current_claim_lines).strip()
|
||||
if len(claim_text) > 30:
|
||||
claims.append({
|
||||
"party_role": current_role,
|
||||
"claim_text": claim_text,
|
||||
"claim_index": claim_index,
|
||||
})
|
||||
claim_index += 1
|
||||
current_claim_lines = [stripped]
|
||||
|
||||
# Save last claim
|
||||
if current_claim_lines:
|
||||
claim_text = "\n".join(current_claim_lines).strip()
|
||||
if len(claim_text) > 30:
|
||||
claims.append({
|
||||
"party_role": current_role,
|
||||
"claim_text": claim_text,
|
||||
"claim_index": claim_index,
|
||||
})
|
||||
|
||||
return claims
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get all block-zayin with content
|
||||
rows = await conn.fetch(
|
||||
"""SELECT c.id as case_id, c.case_number, c.title,
|
||||
db.content
|
||||
FROM decision_blocks db
|
||||
JOIN decisions d ON d.id = db.decision_id
|
||||
JOIN cases c ON c.id = d.case_id
|
||||
WHERE db.block_id = 'block-zayin' AND db.word_count > 0
|
||||
ORDER BY c.case_number"""
|
||||
)
|
||||
|
||||
total_claims = 0
|
||||
|
||||
for row in rows:
|
||||
case_id = row["case_id"]
|
||||
case_number = row["case_number"]
|
||||
text = row["content"]
|
||||
|
||||
claims = extract_claims_from_block(text)
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"תיק: {case_number} — {row['title']}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Delete existing claims for this case
|
||||
await conn.execute("DELETE FROM claims WHERE case_id = $1", case_id)
|
||||
|
||||
role_counts = {}
|
||||
for claim in claims:
|
||||
role = claim["party_role"]
|
||||
role_counts[role] = role_counts.get(role, 0) + 1
|
||||
|
||||
await conn.execute(
|
||||
"""INSERT INTO claims (case_id, party_role, claim_text, claim_index, source_document)
|
||||
VALUES ($1, $2, $3, $4, $5)""",
|
||||
case_id,
|
||||
claim["party_role"],
|
||||
claim["claim_text"],
|
||||
claim["claim_index"],
|
||||
"block-zayin",
|
||||
)
|
||||
|
||||
for role, count in sorted(role_counts.items()):
|
||||
role_heb = {
|
||||
"appellant": "עוררים",
|
||||
"committee": "ועדה מקומית",
|
||||
"respondent": "משיבים",
|
||||
"permit_applicant": "מבקשי היתר",
|
||||
"appraiser": "שמאי",
|
||||
}.get(role, role)
|
||||
print(f" {role_heb:20s} — {count} טענות")
|
||||
|
||||
total_claims += len(claims)
|
||||
print(f" סה\"כ: {len(claims)} טענות")
|
||||
|
||||
# Summary
|
||||
async with pool.acquire() as conn:
|
||||
total = await conn.fetchval("SELECT count(*) FROM claims")
|
||||
by_role = await conn.fetch(
|
||||
"SELECT party_role, count(*) as cnt FROM claims GROUP BY party_role ORDER BY cnt DESC"
|
||||
)
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"סיכום כללי — {total} טענות מ-{len(rows)} החלטות")
|
||||
for r in by_role:
|
||||
print(f" {r['party_role']:20s} — {r['cnt']}")
|
||||
|
||||
await close_pool()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
128
scripts/extract_all_google_vision.py
Normal file
128
scripts/extract_all_google_vision.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Extract ALL PDFs from originals using Google Cloud Vision OCR.
|
||||
Forces OCR on all pages (ignoring broken text layers).
|
||||
Then runs voyage-law-2 embedding benchmark comparing old vs new.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path.home() / ".env")
|
||||
|
||||
import fitz
|
||||
from google.cloud import vision
|
||||
from legal_mcp import config
|
||||
|
||||
API_KEY = config.GOOGLE_CLOUD_VISION_API_KEY
|
||||
client = vision.ImageAnnotatorClient(client_options={"api_key": API_KEY})
|
||||
|
||||
ORIGINALS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/originals")
|
||||
OUTPUT_DIR = ORIGINALS_DIR.parent / "extracted"
|
||||
|
||||
# Hebrew abbreviation quote fixer
|
||||
import re
|
||||
_ABBREV_FIXES = {
|
||||
'עוהייד': 'עוה"ד', 'עוייד': 'עו"ד', 'הנייל': 'הנ"ל',
|
||||
'מצייב': 'מצ"ב', 'ביהמייש': 'ביהמ"ש', 'תייז': 'ת"ז',
|
||||
'עייי': 'ע"י', 'אחייכ': 'אח"כ', 'סייק': 'ס"ק',
|
||||
'דייר': 'ד"ר', 'כדוייח': 'כדו"ח', 'חווייד': 'חוו"ד',
|
||||
'מייר': 'מ"ר', 'יחייד': 'יח"ד', 'בייכ': 'ב"כ',
|
||||
}
|
||||
_ABBREV_PAT = re.compile('|'.join(re.escape(k) for k in sorted(_ABBREV_FIXES, key=len, reverse=True)))
|
||||
|
||||
def fix_quotes(text):
|
||||
return _ABBREV_PAT.sub(lambda m: _ABBREV_FIXES[m.group()], text)
|
||||
|
||||
|
||||
def ocr_page(image_bytes, page_num):
|
||||
image = vision.Image(content=image_bytes)
|
||||
response = client.document_text_detection(
|
||||
image=image,
|
||||
image_context=vision.ImageContext(language_hints=["he"]),
|
||||
)
|
||||
if response.error.message:
|
||||
print(f" ERROR page {page_num}: {response.error.message}")
|
||||
return ""
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
return fix_quotes(text)
|
||||
|
||||
|
||||
def process_pdf(pdf_path):
|
||||
doc = fitz.open(str(pdf_path))
|
||||
page_count = len(doc)
|
||||
pages_text = []
|
||||
t0 = time.time()
|
||||
|
||||
for i in range(page_count):
|
||||
page = doc[i]
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
|
||||
pt = time.time()
|
||||
text = ocr_page(img_bytes, i + 1)
|
||||
elapsed = time.time() - pt
|
||||
pages_text.append(text)
|
||||
print(f" Page {i+1}/{page_count}: {len(text):,} chars, {elapsed:.1f}s")
|
||||
|
||||
doc.close()
|
||||
total_time = time.time() - t0
|
||||
return "\n\n".join(pages_text), page_count, total_time
|
||||
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pdfs = sorted(ORIGINALS_DIR.glob("*.pdf"))
|
||||
print(f"Found {len(pdfs)} PDFs\n")
|
||||
|
||||
results = []
|
||||
total_pages = 0
|
||||
total_time = 0.0
|
||||
|
||||
for pdf in pdfs:
|
||||
out_file = OUTPUT_DIR / f"{pdf.stem}.md"
|
||||
|
||||
# Skip already extracted
|
||||
if out_file.exists() and out_file.stat().st_size > 100:
|
||||
text = out_file.read_text(encoding="utf-8")
|
||||
doc = fitz.open(str(pdf))
|
||||
pages = len(doc)
|
||||
doc.close()
|
||||
print(f"SKIP (exists): {pdf.name} ({pages} pages, {len(text):,} chars)")
|
||||
results.append({"name": pdf.stem, "pages": pages, "chars": len(text), "words": len(text.split()), "time": 0, "skipped": True})
|
||||
total_pages += pages
|
||||
continue
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f" {pdf.name} ({pdf.stat().st_size:,} bytes)")
|
||||
|
||||
text, pages, elapsed = process_pdf(pdf)
|
||||
total_pages += pages
|
||||
total_time += elapsed
|
||||
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
words = len(text.split())
|
||||
print(f" Result: {pages} pages, {len(text):,} chars, {words:,} words, {elapsed:.1f}s")
|
||||
print(f" Saved: {out_file.name}\n")
|
||||
|
||||
results.append({"name": pdf.stem, "pages": pages, "chars": len(text), "words": words, "time": elapsed, "skipped": False})
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"TOTAL: {len(pdfs)} docs, {total_pages} pages, {total_time:.1f}s")
|
||||
est_cost = total_pages * 0.0015
|
||||
print(f"Estimated cost: ${est_cost:.2f}")
|
||||
|
||||
# Save results
|
||||
Path("/home/chaim/legal-ai/data/google-vision-extraction.json").write_text(
|
||||
json.dumps(results, ensure_ascii=False, indent=2)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
66
scripts/extract_google_vision.py
Normal file
66
scripts/extract_google_vision.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Extract text from PDF using Google Cloud Vision API."""
|
||||
|
||||
import io
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import fitz # PyMuPDF for rendering pages to images
|
||||
from google.cloud import vision
|
||||
|
||||
API_KEY = "AIzaSyDZgUsxsy_FHkkREU7R_oQLJALU3_V26j8"
|
||||
|
||||
PDF_PATH = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/originals/מרק קובר-כתב ערר.pdf")
|
||||
OUTPUT_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/extracted")
|
||||
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
client = vision.ImageAnnotatorClient(
|
||||
client_options={"api_key": API_KEY}
|
||||
)
|
||||
|
||||
doc = fitz.open(str(PDF_PATH))
|
||||
page_count = len(doc)
|
||||
print(f"Processing: {PDF_PATH.name} ({page_count} pages)\n")
|
||||
|
||||
pages_text = []
|
||||
total_time = 0.0
|
||||
|
||||
for i in range(page_count):
|
||||
page = doc[i]
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
|
||||
image = vision.Image(content=img_bytes)
|
||||
|
||||
print(f" Page {i+1}/{page_count}...", end=" ", flush=True)
|
||||
t0 = time.time()
|
||||
response = client.document_text_detection(
|
||||
image=image,
|
||||
image_context={"language_hints": ["he"]}
|
||||
)
|
||||
elapsed = time.time() - t0
|
||||
total_time += elapsed
|
||||
|
||||
if response.error.message:
|
||||
print(f"ERROR: {response.error.message}")
|
||||
pages_text.append("")
|
||||
continue
|
||||
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
pages_text.append(text)
|
||||
print(f"{len(text):,} chars, {elapsed:.1f}s")
|
||||
|
||||
doc.close()
|
||||
|
||||
full_text = "\n\n".join(pages_text)
|
||||
out_file = OUTPUT_DIR / f"{PDF_PATH.stem}.md"
|
||||
out_file.write_text(full_text, encoding="utf-8")
|
||||
|
||||
print(f"\nTotal: {len(full_text):,} chars, {len(full_text.split()):,} words, {total_time:.1f}s")
|
||||
print(f"Saved: {out_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
54
scripts/extract_google_vision_single.py
Normal file
54
scripts/extract_google_vision_single.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Extract text from a single PDF using Google Cloud Vision API."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import fitz
|
||||
from google.cloud import vision
|
||||
|
||||
API_KEY = "AIzaSyDZgUsxsy_FHkkREU7R_oQLJALU3_V26j8"
|
||||
OUTPUT_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/extracted")
|
||||
|
||||
def main():
|
||||
pdf_path = Path(sys.argv[1])
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
client = vision.ImageAnnotatorClient(client_options={"api_key": API_KEY})
|
||||
doc = fitz.open(str(pdf_path))
|
||||
page_count = len(doc)
|
||||
print(f"Processing: {pdf_path.name} ({page_count} pages)\n")
|
||||
|
||||
pages_text = []
|
||||
total_time = 0.0
|
||||
|
||||
for i in range(page_count):
|
||||
page = doc[i]
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
image = vision.Image(content=img_bytes)
|
||||
|
||||
print(f" Page {i+1}/{page_count}...", end=" ", flush=True)
|
||||
t0 = time.time()
|
||||
response = client.document_text_detection(image=image, image_context={"language_hints": ["he"]})
|
||||
elapsed = time.time() - t0
|
||||
total_time += elapsed
|
||||
|
||||
if response.error.message:
|
||||
print(f"ERROR: {response.error.message}")
|
||||
pages_text.append("")
|
||||
continue
|
||||
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
pages_text.append(text)
|
||||
print(f"{len(text):,} chars, {elapsed:.1f}s")
|
||||
|
||||
doc.close()
|
||||
full_text = "\n\n".join(pages_text)
|
||||
out_file = OUTPUT_DIR / f"{pdf_path.stem}.md"
|
||||
out_file.write_text(full_text, encoding="utf-8")
|
||||
print(f"\nTotal: {len(full_text):,} chars, {len(full_text.split()):,} words, {total_time:.1f}s")
|
||||
print(f"Saved: {out_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
66
scripts/extract_originals.py
Normal file
66
scripts/extract_originals.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Extract text from original PDF files using Claude Opus Vision OCR."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path.home() / ".env")
|
||||
|
||||
from legal_mcp.services import extractor
|
||||
|
||||
ORIGINALS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/originals")
|
||||
OUTPUT_DIR = ORIGINALS_DIR / "extracted"
|
||||
|
||||
|
||||
async def main():
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
pdfs = sorted(ORIGINALS_DIR.glob("*.pdf"))
|
||||
print(f"Found {len(pdfs)} PDFs\n")
|
||||
|
||||
total_cost = 0.0
|
||||
total_pages = 0
|
||||
total_time = 0.0
|
||||
|
||||
for pdf in pdfs:
|
||||
print(f"{'=' * 60}")
|
||||
print(f"Processing: {pdf.name}")
|
||||
print(f" Size: {pdf.stat().st_size:,} bytes")
|
||||
|
||||
t0 = time.time()
|
||||
text, page_count = await extractor.extract_text(str(pdf))
|
||||
elapsed = time.time() - t0
|
||||
|
||||
total_pages += page_count
|
||||
total_time += elapsed
|
||||
|
||||
# Estimate cost (Opus: $15/M input, $75/M output, ~1000 tokens per image)
|
||||
# Rough: ~$0.05 per page for image input + output
|
||||
est_cost = page_count * 0.05
|
||||
total_cost += est_cost
|
||||
|
||||
# Save extracted text
|
||||
out_file = OUTPUT_DIR / f"{pdf.stem}.md"
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
print(f" Pages: {page_count}")
|
||||
print(f" Extracted: {len(text):,} chars, {len(text.split()):,} words")
|
||||
print(f" Time: {elapsed:.1f}s ({elapsed/max(page_count,1):.1f}s/page)")
|
||||
print(f" Est. cost: ${est_cost:.3f}")
|
||||
print(f" Saved to: {out_file.name}")
|
||||
print()
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f"TOTAL")
|
||||
print(f" Documents: {len(pdfs)}")
|
||||
print(f" Pages: {total_pages}")
|
||||
print(f" Time: {total_time:.1f}s")
|
||||
print(f" Est. cost: ${total_cost:.3f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
113
scripts/extract_originals_ocr.py
Normal file
113
scripts/extract_originals_ocr.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Extract text from original PDF files using Claude Opus Vision OCR on ALL pages.
|
||||
|
||||
Forces Vision OCR regardless of embedded text layer (which may be broken).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path.home() / ".env")
|
||||
|
||||
import anthropic
|
||||
import fitz
|
||||
from legal_mcp import config
|
||||
|
||||
client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
|
||||
MODEL = "claude-opus-4-20250514"
|
||||
|
||||
ORIGINALS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/originals")
|
||||
OUTPUT_DIR = ORIGINALS_DIR.parent / "extracted"
|
||||
|
||||
|
||||
async def ocr_page(image_bytes: bytes, page_num: int) -> str:
|
||||
b64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
message = client.messages.create(
|
||||
model=MODEL,
|
||||
max_tokens=4096,
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {"type": "base64", "media_type": "image/png", "data": b64_image},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"חלץ את כל הטקסט מהתמונה הזו. זהו מסמך משפטי בעברית. "
|
||||
"שמור על מבנה הפסקאות המקורי. "
|
||||
"אם יש כותרות, סמן אותן. "
|
||||
"החזר רק את הטקסט המחולץ, ללא הערות נוספות."
|
||||
),
|
||||
},
|
||||
],
|
||||
}],
|
||||
)
|
||||
return message.content[0].text
|
||||
|
||||
|
||||
async def process_pdf(pdf_path: Path) -> tuple[str, int, float, int, int]:
|
||||
doc = fitz.open(str(pdf_path))
|
||||
page_count = len(doc)
|
||||
pages_text = []
|
||||
total_input = 0
|
||||
total_output = 0
|
||||
|
||||
t0 = time.time()
|
||||
for i in range(page_count):
|
||||
page = doc[i]
|
||||
pix = page.get_pixmap(dpi=200)
|
||||
img_bytes = pix.tobytes("png")
|
||||
|
||||
print(f" Page {i+1}/{page_count}...", end=" ", flush=True)
|
||||
pt = time.time()
|
||||
text = await ocr_page(img_bytes, i + 1)
|
||||
elapsed = time.time() - pt
|
||||
pages_text.append(text)
|
||||
print(f"{len(text):,} chars, {elapsed:.1f}s")
|
||||
|
||||
doc.close()
|
||||
total_time = time.time() - t0
|
||||
full_text = "\n\n".join(pages_text)
|
||||
return full_text, page_count, total_time
|
||||
|
||||
|
||||
async def main():
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pdfs = sorted(ORIGINALS_DIR.glob("*.pdf"))
|
||||
print(f"Found {len(pdfs)} PDFs — extracting ALL pages with {MODEL}\n")
|
||||
|
||||
total_pages = 0
|
||||
total_time = 0.0
|
||||
|
||||
for pdf in pdfs:
|
||||
print(f"{'=' * 60}")
|
||||
print(f" {pdf.name} ({pdf.stat().st_size:,} bytes)")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
text, pages, elapsed = await process_pdf(pdf)
|
||||
total_pages += pages
|
||||
total_time += elapsed
|
||||
|
||||
out_file = OUTPUT_DIR / f"{pdf.stem}.md"
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
print(f" Result: {pages} pages, {len(text):,} chars, {len(text.split()):,} words")
|
||||
print(f" Time: {elapsed:.1f}s ({elapsed/max(pages,1):.1f}s/page)")
|
||||
print(f" Saved: {out_file.name}\n")
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f"TOTAL: {len(pdfs)} docs, {total_pages} pages, {total_time:.1f}s")
|
||||
est_cost = total_pages * 0.05
|
||||
print(f"Estimated cost: ${est_cost:.2f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
177
scripts/generate-embeddings.py
Normal file
177
scripts/generate-embeddings.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate embeddings for decision blocks and case law.
|
||||
|
||||
Creates:
|
||||
- paragraph_embeddings: for each decision block with content
|
||||
- case_law_embeddings: for each case law summary
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
from legal_mcp.services.embeddings import embed_texts
|
||||
from legal_mcp import config
|
||||
|
||||
|
||||
async def generate_block_embeddings(conn) -> int:
|
||||
"""Generate embeddings for decision blocks.
|
||||
|
||||
First creates decision_paragraphs records from block content,
|
||||
then generates embeddings in paragraph_embeddings.
|
||||
"""
|
||||
blocks = await conn.fetch(
|
||||
"""SELECT db.id as block_id, db.decision_id, db.block_id as block_type,
|
||||
db.content, db.word_count, c.case_number
|
||||
FROM decision_blocks db
|
||||
JOIN decisions d ON d.id = db.decision_id
|
||||
JOIN cases c ON c.id = d.case_id
|
||||
WHERE db.word_count > 10
|
||||
AND db.block_id NOT IN ('block-alef', 'block-bet', 'block-gimel', 'block-dalet')
|
||||
ORDER BY c.case_number, db.block_index"""
|
||||
)
|
||||
|
||||
if not blocks:
|
||||
print(" אין בלוקים ליצירת embeddings")
|
||||
return 0
|
||||
|
||||
print(f" מעבד {len(blocks)} בלוקים...")
|
||||
|
||||
# Create paragraphs and collect texts for embedding
|
||||
para_records = []
|
||||
para_number = 1
|
||||
|
||||
for block in blocks:
|
||||
content = block["content"]
|
||||
words = content.split()
|
||||
|
||||
# Split into chunks for embedding
|
||||
if len(words) <= 600:
|
||||
chunk_texts = [content]
|
||||
else:
|
||||
chunk_texts = []
|
||||
for start in range(0, len(words), 400):
|
||||
chunk_words = words[start:start + 500]
|
||||
if len(chunk_words) > 50:
|
||||
chunk_texts.append(" ".join(chunk_words))
|
||||
|
||||
for chunk_text in chunk_texts:
|
||||
# Create decision_paragraph record
|
||||
para_id = await conn.fetchval(
|
||||
"""INSERT INTO decision_paragraphs
|
||||
(block_id, paragraph_number, content, word_count)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id""",
|
||||
block["block_id"],
|
||||
para_number,
|
||||
chunk_text,
|
||||
len(chunk_text.split()),
|
||||
)
|
||||
if para_id:
|
||||
para_records.append({
|
||||
"para_id": para_id,
|
||||
"text": chunk_text,
|
||||
"case_number": block["case_number"],
|
||||
})
|
||||
para_number += 1
|
||||
|
||||
if not para_records:
|
||||
print(" אין פסקאות חדשות")
|
||||
return 0
|
||||
|
||||
print(f" {len(para_records)} פסקאות נוצרו, מייצר embeddings...")
|
||||
|
||||
# Generate embeddings in batches
|
||||
texts = [p["text"] for p in para_records]
|
||||
embeddings = await embed_texts(texts, input_type="document")
|
||||
|
||||
# Store embeddings
|
||||
count = 0
|
||||
for para, embedding in zip(para_records, embeddings):
|
||||
await conn.execute(
|
||||
"""INSERT INTO paragraph_embeddings (paragraph_id, embedding)
|
||||
VALUES ($1, $2)""",
|
||||
para["para_id"],
|
||||
embedding,
|
||||
)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def generate_case_law_embeddings(conn) -> int:
|
||||
"""Generate embeddings for case law summaries."""
|
||||
cases = await conn.fetch(
|
||||
"""SELECT id, case_number, case_name, summary, key_quote
|
||||
FROM case_law
|
||||
WHERE summary != '' OR key_quote != ''"""
|
||||
)
|
||||
|
||||
# Filter out existing
|
||||
existing = await conn.fetch("SELECT case_law_id FROM case_law_embeddings")
|
||||
existing_ids = {r["case_law_id"] for r in existing}
|
||||
|
||||
to_embed = [c for c in cases if c["id"] not in existing_ids]
|
||||
|
||||
if not to_embed:
|
||||
print(" אין פסיקה חדשה ליצירת embeddings")
|
||||
return 0
|
||||
|
||||
print(f" מייצר embeddings ל-{len(to_embed)} תקדימים...")
|
||||
|
||||
texts = []
|
||||
for c in to_embed:
|
||||
# Combine case info into a searchable text
|
||||
text = f"{c['case_number']} {c['case_name']}: {c['summary']}"
|
||||
if c["key_quote"]:
|
||||
text += f" ציטוט: {c['key_quote']}"
|
||||
texts.append(text)
|
||||
|
||||
embeddings = await embed_texts(texts, input_type="document")
|
||||
|
||||
count = 0
|
||||
for case, embedding in zip(to_embed, embeddings):
|
||||
await conn.execute(
|
||||
"""INSERT INTO case_law_embeddings (case_law_id, chunk_text, embedding)
|
||||
VALUES ($1, $2, $3)""",
|
||||
case["id"],
|
||||
f"{case['case_number']} {case['case_name']}: {case['summary']}",
|
||||
embedding,
|
||||
)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
print("שלב 1: embeddings לבלוקי החלטה")
|
||||
block_count = await generate_block_embeddings(conn)
|
||||
print(f" ✅ {block_count} embeddings נוצרו")
|
||||
|
||||
print("\nשלב 2: embeddings לפסיקה")
|
||||
cl_count = await generate_case_law_embeddings(conn)
|
||||
print(f" ✅ {cl_count} embeddings נוצרו")
|
||||
|
||||
# Summary
|
||||
para_total = await conn.fetchval("SELECT count(*) FROM paragraph_embeddings")
|
||||
cl_total = await conn.fetchval("SELECT count(*) FROM case_law_embeddings")
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"\nסיכום:")
|
||||
print(f" סה\"כ paragraph_embeddings: {para_total}")
|
||||
print(f" סה\"כ case_law_embeddings: {cl_total}")
|
||||
print(f" מודל: {config.VOYAGE_MODEL}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
202
scripts/import-final-decisions.py
Normal file
202
scripts/import-final-decisions.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import 6 final signed decisions: extract text, store in DB."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
import fitz # PyMuPDF
|
||||
from docx import Document as DocxDocument
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# 6 Final Decisions
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
FINAL_DECISIONS = [
|
||||
{
|
||||
"case_number": "1180-1181",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/ערר 1180-1181 הכט/החלטה/הכט 1180-1181.pdf",
|
||||
"title": "החלטה סופית — הכט 1180-1181",
|
||||
"outcome": "rejected",
|
||||
"decision_date": date(2026, 2, 5),
|
||||
},
|
||||
{
|
||||
"case_number": "8255-25",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/בל\"מ 8255-25 אפרים אבי נ' הוועדה המקומית לתכנון ובניה/החלטה/אליהו הרנון - להפצה.docx",
|
||||
"title": "החלטה סופית — אפרים אבי 8255-25",
|
||||
"outcome": "rejected",
|
||||
"decision_date": None,
|
||||
},
|
||||
{
|
||||
"case_number": "8007-24",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/ערר 8007-24-עומר דרוויש-ערר על שומה מכרעת/החלטה/החלטה-סופית.docx",
|
||||
"title": "החלטה סופית — עומר דרוויש 8007-24",
|
||||
"outcome": "",
|
||||
"decision_date": None,
|
||||
},
|
||||
{
|
||||
"case_number": "1113/25",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1113-25-אייל-מבורך/החלטה/החלטה-1113-25-טיוטה-סופית.docx",
|
||||
"title": "החלטה סופית — מבורך 1113-25",
|
||||
"outcome": "",
|
||||
"decision_date": None,
|
||||
},
|
||||
{
|
||||
"case_number": "1126/25+1141/25",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1126-25-תמא-38-בית-הכרם/החלטה/בית הכרם-טיוטת החלטה-9.pdf",
|
||||
"title": "החלטה סופית — בית הכרם 1126/25",
|
||||
"outcome": "partial",
|
||||
"decision_date": date(2026, 3, 1),
|
||||
},
|
||||
{
|
||||
"case_number": "1128/25",
|
||||
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1128-25-שטרית/החלטה/1128-25 החלטה להפצה.pdf",
|
||||
"title": "החלטה סופית — שטרית 1128-25",
|
||||
"outcome": "",
|
||||
"decision_date": None,
|
||||
},
|
||||
]
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def extract_pdf_text(file_path: Path) -> str:
|
||||
"""Extract text from PDF using PyMuPDF."""
|
||||
doc = fitz.open(str(file_path))
|
||||
text_parts = []
|
||||
for page in doc:
|
||||
text_parts.append(page.get_text())
|
||||
doc.close()
|
||||
return "\n".join(text_parts)
|
||||
|
||||
|
||||
def extract_docx_text(file_path: Path) -> str:
|
||||
"""Extract text from DOCX."""
|
||||
doc = DocxDocument(str(file_path))
|
||||
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
|
||||
|
||||
|
||||
def extract_text(file_path: Path) -> str:
|
||||
"""Extract text based on file extension."""
|
||||
suffix = file_path.suffix.lower()
|
||||
if suffix == ".pdf":
|
||||
return extract_pdf_text(file_path)
|
||||
elif suffix == ".docx":
|
||||
return extract_docx_text(file_path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported format: {suffix}")
|
||||
|
||||
|
||||
def count_words(text: str) -> int:
|
||||
return len(text.split())
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
for d in FINAL_DECISIONS:
|
||||
file_path = PROJECT_ROOT / d["file_path"]
|
||||
if not file_path.exists():
|
||||
print(f"❌ קובץ לא נמצא: {file_path}")
|
||||
continue
|
||||
|
||||
# Extract text
|
||||
print(f"\nמחלץ טקסט: {d['title']}...")
|
||||
text = extract_text(file_path)
|
||||
word_count = count_words(text)
|
||||
print(f" {word_count} מילים, {len(text)} תווים")
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get case_id
|
||||
case_id = await conn.fetchval(
|
||||
"SELECT id FROM cases WHERE case_number = $1", d["case_number"]
|
||||
)
|
||||
if not case_id:
|
||||
print(f" ⚠ תיק {d['case_number']} לא נמצא ב-DB — מדלג")
|
||||
continue
|
||||
|
||||
# Register document
|
||||
existing_doc = await conn.fetchval(
|
||||
"SELECT id FROM documents WHERE file_path = $1",
|
||||
str(file_path),
|
||||
)
|
||||
if existing_doc:
|
||||
doc_id = existing_doc
|
||||
print(f" מסמך כבר קיים ב-DB: {doc_id}")
|
||||
# Update text
|
||||
await conn.execute(
|
||||
"""UPDATE documents SET extracted_text = $1, extraction_status = 'completed'
|
||||
WHERE id = $2""",
|
||||
text, doc_id,
|
||||
)
|
||||
else:
|
||||
doc_id = await conn.fetchval(
|
||||
"""INSERT INTO documents (case_id, doc_type, title, file_path, extracted_text, extraction_status, page_count)
|
||||
VALUES ($1, 'decision', $2, $3, $4, 'completed', $5)
|
||||
RETURNING id""",
|
||||
case_id, d["title"], str(file_path), text,
|
||||
len(fitz.open(str(file_path))) if file_path.suffix == ".pdf" else None,
|
||||
)
|
||||
print(f" מסמך נרשם: {doc_id}")
|
||||
|
||||
# Create/update decision record
|
||||
existing_decision = await conn.fetchval(
|
||||
"SELECT id FROM decisions WHERE case_id = $1", case_id
|
||||
)
|
||||
if existing_decision:
|
||||
await conn.execute(
|
||||
"""UPDATE decisions SET status = 'final', outcome = $1, total_words = $2,
|
||||
decision_date = $3, updated_at = now() WHERE id = $4""",
|
||||
d["outcome"], word_count, d["decision_date"], existing_decision,
|
||||
)
|
||||
decision_id = existing_decision
|
||||
print(f" החלטה עודכנה: {decision_id}")
|
||||
else:
|
||||
decision_id = await conn.fetchval(
|
||||
"""INSERT INTO decisions (case_id, version, status, outcome, outcome_summary,
|
||||
total_words, decision_date, author)
|
||||
VALUES ($1, 1, 'final', $2, $3, $4, $5, 'דפנה תמיר')
|
||||
RETURNING id""",
|
||||
case_id, d["outcome"], d["title"], word_count, d["decision_date"],
|
||||
)
|
||||
print(f" החלטה נוצרה: {decision_id}")
|
||||
|
||||
# Update case status
|
||||
await conn.execute(
|
||||
"UPDATE cases SET status = 'final', expected_outcome = $1, updated_at = now() WHERE id = $2",
|
||||
d["outcome"], case_id,
|
||||
)
|
||||
|
||||
print(f" ✅ הושלם: {d['case_number']}")
|
||||
|
||||
# Summary
|
||||
async with pool.acquire() as conn:
|
||||
doc_count = await conn.fetchval(
|
||||
"SELECT count(*) FROM documents WHERE doc_type = 'decision' AND extraction_status = 'completed'"
|
||||
)
|
||||
dec_count = await conn.fetchval(
|
||||
"SELECT count(*) FROM decisions WHERE status = 'final'"
|
||||
)
|
||||
total_words = await conn.fetchval(
|
||||
"SELECT sum(total_words) FROM decisions WHERE status = 'final'"
|
||||
)
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"✅ סה\"כ מסמכי החלטה: {doc_count}")
|
||||
print(f"✅ סה\"כ החלטות סופיות: {dec_count}")
|
||||
print(f"✅ סה\"כ מילים: {total_words:,}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
118
scripts/link-claims-to-discussion.py
Normal file
118
scripts/link-claims-to-discussion.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link claims to discussion paragraphs using semantic similarity.
|
||||
|
||||
For each claim, finds the most similar paragraph in block-yod of the same decision.
|
||||
Updates claims.addressed_in_paragraph with the paragraph number.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
from legal_mcp.services.embeddings import embed_texts
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get all cases with both claims and discussion blocks
|
||||
cases = await conn.fetch(
|
||||
"""SELECT DISTINCT c.id as case_id, c.case_number
|
||||
FROM cases c
|
||||
JOIN claims cl ON cl.case_id = c.id
|
||||
JOIN decisions d ON d.case_id = c.id
|
||||
JOIN decision_blocks db ON db.decision_id = d.id AND db.block_id = 'block-yod' AND db.word_count > 0
|
||||
ORDER BY c.case_number"""
|
||||
)
|
||||
|
||||
total_linked = 0
|
||||
|
||||
for case in cases:
|
||||
case_id = case["case_id"]
|
||||
case_number = case["case_number"]
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
# Get claims for this case
|
||||
claims = await conn.fetch(
|
||||
"SELECT id, claim_text, party_role, claim_index FROM claims WHERE case_id = $1 ORDER BY claim_index",
|
||||
case_id,
|
||||
)
|
||||
|
||||
# Get discussion paragraphs (split block-yod into paragraphs)
|
||||
yod_content = await conn.fetchval(
|
||||
"""SELECT db.content FROM decision_blocks db
|
||||
JOIN decisions d ON d.id = db.decision_id
|
||||
WHERE d.case_id = $1 AND db.block_id = 'block-yod'""",
|
||||
case_id,
|
||||
)
|
||||
|
||||
if not yod_content or not claims:
|
||||
continue
|
||||
|
||||
# Split discussion into paragraphs
|
||||
disc_paragraphs = [p.strip() for p in yod_content.split("\n") if p.strip() and len(p.strip()) > 30]
|
||||
|
||||
if not disc_paragraphs:
|
||||
continue
|
||||
|
||||
print(f"\n{case_number}: {len(claims)} טענות ← {len(disc_paragraphs)} פסקאות דיון")
|
||||
|
||||
# Embed all claims and discussion paragraphs
|
||||
claim_texts = [c["claim_text"][:500] for c in claims]
|
||||
all_texts = claim_texts + disc_paragraphs
|
||||
|
||||
embeddings = await embed_texts(all_texts, input_type="document")
|
||||
|
||||
claim_embeddings = embeddings[:len(claims)]
|
||||
disc_embeddings = embeddings[len(claims):]
|
||||
|
||||
# For each claim, find the best matching discussion paragraph
|
||||
linked = 0
|
||||
async with pool.acquire() as conn:
|
||||
for i, claim in enumerate(claims):
|
||||
claim_emb = claim_embeddings[i]
|
||||
|
||||
# Cosine similarity
|
||||
best_score = -1
|
||||
best_para_idx = -1
|
||||
for j, disc_emb in enumerate(disc_embeddings):
|
||||
dot = sum(a * b for a, b in zip(claim_emb, disc_emb))
|
||||
norm_a = sum(a * a for a in claim_emb) ** 0.5
|
||||
norm_b = sum(b * b for b in disc_emb) ** 0.5
|
||||
score = dot / (norm_a * norm_b) if norm_a > 0 and norm_b > 0 else 0
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_para_idx = j
|
||||
|
||||
if best_para_idx >= 0 and best_score > 0.3:
|
||||
# paragraph_number is 1-indexed
|
||||
para_num = best_para_idx + 1
|
||||
await conn.execute(
|
||||
"UPDATE claims SET addressed_in_paragraph = $1 WHERE id = $2",
|
||||
para_num, claim["id"],
|
||||
)
|
||||
linked += 1
|
||||
|
||||
total_linked += linked
|
||||
print(f" קושרו: {linked}/{len(claims)} טענות (ציון מינימלי: 0.3)")
|
||||
|
||||
# Summary
|
||||
async with pool.acquire() as conn:
|
||||
total_claims = await conn.fetchval("SELECT count(*) FROM claims")
|
||||
linked_claims = await conn.fetchval("SELECT count(*) FROM claims WHERE addressed_in_paragraph IS NOT NULL")
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"סיכום: {linked_claims}/{total_claims} טענות קושרו לפסקאות דיון ({linked_claims/total_claims*100:.0f}%)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
53
scripts/notify.py
Executable file
53
scripts/notify.py
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send email notification from agents via SMTP.
|
||||
|
||||
Usage:
|
||||
python3 scripts/notify.py "subject" "body"
|
||||
python3 scripts/notify.py "subject" --file /path/to/body.md
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
import sys
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
SMTP_HOST = "smtp.gmail.com"
|
||||
SMTP_PORT = 587
|
||||
FROM_EMAIL = "notify@marcus-law.co.il"
|
||||
FROM_PASS = "vuva jwed lbuz xjds"
|
||||
TO_EMAIL = "paperclip+chaim@marcus-law.co.il"
|
||||
|
||||
|
||||
def send(subject: str, body: str) -> bool:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"עוזר משפטי <{FROM_EMAIL}>"
|
||||
msg["To"] = TO_EMAIL
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
|
||||
server.starttls()
|
||||
server.login(FROM_EMAIL, FROM_PASS)
|
||||
server.sendmail(FROM_EMAIL, TO_EMAIL, msg.as_string())
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Email failed: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: notify.py 'subject' 'body' OR notify.py 'subject' --file path")
|
||||
sys.exit(1)
|
||||
|
||||
subject = sys.argv[1]
|
||||
if sys.argv[2] == "--file":
|
||||
body = open(sys.argv[3], encoding="utf-8").read()
|
||||
else:
|
||||
body = sys.argv[2]
|
||||
|
||||
if send(subject, body):
|
||||
print(f"Sent to {TO_EMAIL}")
|
||||
else:
|
||||
sys.exit(1)
|
||||
382
scripts/proofread_training_corpus.py
Normal file
382
scripts/proofread_training_corpus.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""Proofread training corpus: strip Nevo additions from DOCX/PDF, output clean Markdown.
|
||||
|
||||
Nevo DOCX additions:
|
||||
Front: ספרות / חקיקה שאוזכרה / מיני-רציו / topic tags / Nevo summary paragraphs
|
||||
Back: 5129371512937154678313 / "בעניין עריכה ושינויים" link / "54678313-..." / "נוסח מסמך זה כפוף"
|
||||
|
||||
Nevo PDF additions:
|
||||
"עמוד X מתוך Y" header on every page
|
||||
|
||||
PDF text extraction uses Google Cloud Vision OCR — PyMuPDF fragments Hebrew RTL
|
||||
text unusably (words split mid-word, reading order broken). OCR gives clean output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import fitz
|
||||
from docx import Document
|
||||
|
||||
# Load GOOGLE_CLOUD_VISION_API_KEY from ~/.env if not already set
|
||||
if not os.environ.get("GOOGLE_CLOUD_VISION_API_KEY"):
|
||||
env_path = Path.home() / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text().splitlines():
|
||||
if line.startswith("GOOGLE_CLOUD_VISION_API_KEY="):
|
||||
os.environ["GOOGLE_CLOUD_VISION_API_KEY"] = line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
break
|
||||
|
||||
from google.cloud import vision # noqa: E402
|
||||
|
||||
TRAINING_DIR = Path("/home/chaim/legal-ai/data/training")
|
||||
OUTPUT_DIR = TRAINING_DIR / "proofread"
|
||||
RAW_DIR = TRAINING_DIR / "raw"
|
||||
|
||||
# ── Nevo pattern detection ────────────────────────────────────────
|
||||
|
||||
NEVO_PREAMBLE_HEADERS = (
|
||||
"ספרות:",
|
||||
"חקיקה שאוזכרה:",
|
||||
"מיני-רציו:",
|
||||
)
|
||||
|
||||
# Strong decision-opening patterns — highly distinctive first words of real decision
|
||||
# body. These rarely appear inside Nevo's own summary block, so first match wins.
|
||||
DECISION_OPENING = re.compile(
|
||||
r"^(עניינו\s|ענייננו\s|עסקינן\s|בפנינו\s|לפנינו\s|בערר\s+שלפנינו|זהו\s+ערר)"
|
||||
)
|
||||
|
||||
# Section headers that definitively mark decision body start.
|
||||
DECISION_SECTION_HEADERS = {
|
||||
"רקע",
|
||||
"פתח דבר",
|
||||
"תמצית טענות הצדדים",
|
||||
"העובדות",
|
||||
"הרקע העובדתי",
|
||||
"מבוא",
|
||||
}
|
||||
|
||||
# Nevo postamble markers — everything from first match onwards is stripped.
|
||||
NEVO_POSTAMBLE_MARKERS = (
|
||||
"5129371512937154678313",
|
||||
"בעניין עריכה ושינויים במסמכי פסיקה",
|
||||
"נוסח מסמך זה כפוף לשינויי ניסוח ועריכה",
|
||||
)
|
||||
|
||||
# Nevo inline watermark codes — appear as prefixes embedded in real paragraphs
|
||||
# (e.g. "5129371ניתנה פה אחד" or "054678313האם ההיתר..."). These must be
|
||||
# stripped from paragraph content, not used as postamble boundaries.
|
||||
NEVO_INLINE_CODE_RE = re.compile(r"^0?(5129371|54678313)\d*")
|
||||
|
||||
# Nevo PDF page header: "עמוד X מתוך Y" or "עמוד X בן Y" (Hebrew variants)
|
||||
PDF_PAGE_HEADER_RE = re.compile(
|
||||
r"\s*עמוד\s*\n?\s*\d+\s*\n?\s*(?:מתוך|בן)\s*\n?\s*\d+\s*"
|
||||
)
|
||||
# Short orphan lines starting with "עמוד" — OCR artifacts from merged footer text
|
||||
# (e.g. "עמודירבי", "עמוד :", "עמודי", "עמוד ר"). Conservative: up to 12 chars.
|
||||
PDF_PAGE_ORPHAN_RE = re.compile(r"(?m)^עמוד[^\n]{0,12}$")
|
||||
# "עמוד" followed by number (with optional garbled Nevo URL line after)
|
||||
PDF_PAGE_BLOCK_RE = re.compile(
|
||||
r"(?m)^\s*עמוד\s*\n\s*\d+[·.]?\s*\n[^\n]*\n", re.UNICODE
|
||||
)
|
||||
# Standalone "עמוד N" at line start
|
||||
PDF_PAGE_NUM_LINE_RE = re.compile(r"(?m)^\s*עמוד\s*\n?\s*\d+[·.]?\s*$")
|
||||
# Nevo watermark URL (and common OCR-garbled variants)
|
||||
NEVO_URL_RE = re.compile(
|
||||
r"(nevo\.co\.il|neto\.co\.il|netocoal|neetocoal|nevocoal|nevo\.co|rawo\.co\.il)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def find_decision_start(paragraphs: list[str]) -> int:
|
||||
"""Find index of first real decision paragraph, skipping Nevo preamble.
|
||||
|
||||
Strategy:
|
||||
1. If no Nevo headers present → start at 0.
|
||||
2. Otherwise, scan past Nevo headers; look for first paragraph matching
|
||||
DECISION_OPENING regex or DECISION_SECTION_HEADERS.
|
||||
3. Fallback: first paragraph after "ועדת הערר ... קבעה כלהלן:" bullet block
|
||||
that doesn't look like summary (heuristic: longer, has proper sentence).
|
||||
"""
|
||||
has_nevo_preamble = any(
|
||||
any(p.startswith(h) for h in NEVO_PREAMBLE_HEADERS) for p in paragraphs[:10]
|
||||
)
|
||||
if not has_nevo_preamble:
|
||||
return 0
|
||||
|
||||
# Scan for strong decision-opening markers
|
||||
for i, p in enumerate(paragraphs):
|
||||
stripped = p.strip()
|
||||
if stripped in DECISION_SECTION_HEADERS:
|
||||
return i
|
||||
if DECISION_OPENING.match(stripped):
|
||||
return i
|
||||
|
||||
# Fallback: find "ועדת הערר ... קבעה כלהלן" and take first long para after bullets
|
||||
for i, p in enumerate(paragraphs):
|
||||
if "קבעה כלהלן" in p or "קבעה את הדברים הבאים" in p:
|
||||
# Skip summary paragraphs (Nevo typically has 3-8 of these)
|
||||
for j in range(i + 1, min(i + 15, len(paragraphs))):
|
||||
if len(paragraphs[j]) > 80 and not paragraphs[j].strip().startswith("*"):
|
||||
# Check if this looks like real decision content
|
||||
return j
|
||||
break
|
||||
|
||||
# Last resort: strip only the first 10 paragraphs of preamble
|
||||
return min(10, len(paragraphs) - 1)
|
||||
|
||||
|
||||
def find_decision_end(paragraphs: list[str]) -> int:
|
||||
"""Find exclusive end index: first paragraph that is a Nevo postamble marker."""
|
||||
for i, p in enumerate(paragraphs):
|
||||
for marker in NEVO_POSTAMBLE_MARKERS:
|
||||
if marker in p:
|
||||
return i
|
||||
return len(paragraphs)
|
||||
|
||||
|
||||
# ── DOCX proofreading ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _strip_inline_nevo_codes(paragraphs: list[str]) -> list[str]:
|
||||
"""Remove Nevo inline watermark codes from paragraph prefixes; drop pure-code paras."""
|
||||
out: list[str] = []
|
||||
for p in paragraphs:
|
||||
stripped = NEVO_INLINE_CODE_RE.sub("", p).strip()
|
||||
if stripped:
|
||||
out.append(stripped)
|
||||
return out
|
||||
|
||||
|
||||
def proofread_docx(path: Path) -> tuple[str, dict]:
|
||||
"""Extract clean decision text from Nevo DOCX. Returns (markdown, stats)."""
|
||||
doc = Document(str(path))
|
||||
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
|
||||
|
||||
start = find_decision_start(paragraphs)
|
||||
end = find_decision_end(paragraphs)
|
||||
|
||||
clean = _strip_inline_nevo_codes(paragraphs[start:end])
|
||||
md = "\n\n".join(clean)
|
||||
|
||||
return md, {
|
||||
"total_paragraphs": len(paragraphs),
|
||||
"preamble_stripped": start,
|
||||
"postamble_stripped": len(paragraphs) - end,
|
||||
"clean_paragraphs": len(clean),
|
||||
}
|
||||
|
||||
|
||||
# ── PDF proofreading (Google Vision OCR) ──────────────────────────
|
||||
|
||||
_vision_client: vision.ImageAnnotatorClient | None = None
|
||||
|
||||
|
||||
def _get_vision_client() -> vision.ImageAnnotatorClient:
|
||||
global _vision_client
|
||||
if _vision_client is None:
|
||||
api_key = os.environ.get("GOOGLE_CLOUD_VISION_API_KEY")
|
||||
if not api_key:
|
||||
raise RuntimeError("GOOGLE_CLOUD_VISION_API_KEY not set")
|
||||
_vision_client = vision.ImageAnnotatorClient(
|
||||
client_options={"api_key": api_key}
|
||||
)
|
||||
return _vision_client
|
||||
|
||||
|
||||
# Hebrew abbreviation quote fixes — Google Vision renders ״ as 'יי'
|
||||
_HEBREW_ABBREV_FIXES: dict[str, str] = {
|
||||
"עוהייד": 'עוה"ד',
|
||||
"עוייד": 'עו"ד',
|
||||
"הנייל": 'הנ"ל',
|
||||
"מצייב": 'מצ"ב',
|
||||
"ביהמייש": 'ביהמ"ש',
|
||||
"תייז": 'ת"ז',
|
||||
"עייי": 'ע"י',
|
||||
"אחייכ": 'אח"כ',
|
||||
"סייק": 'ס"ק',
|
||||
"דייר": 'ד"ר',
|
||||
"חווייד": 'חוו"ד',
|
||||
"מייר": 'מ"ר',
|
||||
"יחייד": 'יח"ד',
|
||||
"בייכ": 'ב"כ',
|
||||
"בייה": 'ב"ה',
|
||||
"שייח": 'ש"ח',
|
||||
"יוייר": 'יו"ר',
|
||||
"בליימ": 'בל"מ',
|
||||
"תבייע": 'תב"ע',
|
||||
"תמייא": 'תמ"א',
|
||||
"סייה": 'ס"ה',
|
||||
"שייפ": 'ש"פ',
|
||||
"שצייפ": 'שצ"פ',
|
||||
"שבייצ": 'שב"צ',
|
||||
"עסיים": 'עס"ם',
|
||||
"הייה": 'ה"ה',
|
||||
"פסייד": 'פס"ד',
|
||||
"תיידא": 'תיד"א',
|
||||
"בגייץ": 'בג"ץ',
|
||||
"עתיים": 'עת"ם',
|
||||
"עעיים": 'עע"ם',
|
||||
# Hebrew calendar day prefixes (כ"א .. כ"ט etc.)
|
||||
"כייא": 'כ"א', "כייב": 'כ"ב', "כייג": 'כ"ג', "כייד": 'כ"ד',
|
||||
"כייה": 'כ"ה', "כייו": 'כ"ו', "כייז": 'כ"ז', "כייח": 'כ"ח', "כייט": 'כ"ט',
|
||||
"לייא": 'ל"א',
|
||||
"יייא": 'י"א', "יייב": 'י"ב', "יייג": 'י"ג', "יייד": 'י"ד',
|
||||
"טייו": 'ט"ו', "טייז": 'ט"ז', "יייז": 'י"ז', "יייח": 'י"ח', "יייט": 'י"ט',
|
||||
# Hebrew calendar years (תשפ"ה, תשפ"ד...)
|
||||
"תשפייא": 'תשפ"א', "תשפייב": 'תשפ"ב', "תשפייג": 'תשפ"ג',
|
||||
"תשפייד": 'תשפ"ד', "תשפייה": 'תשפ"ה', "תשפייו": 'תשפ"ו',
|
||||
"תשפיין": 'תשפ"ן',
|
||||
}
|
||||
_ABBREV_PATTERN = re.compile(
|
||||
"|".join(re.escape(k) for k in sorted(_HEBREW_ABBREV_FIXES, key=len, reverse=True))
|
||||
)
|
||||
|
||||
|
||||
def _fix_hebrew_quotes(text: str) -> str:
|
||||
return _ABBREV_PATTERN.sub(lambda m: _HEBREW_ABBREV_FIXES[m.group()], text)
|
||||
|
||||
|
||||
def _ocr_page_image(image_bytes: bytes, page_num: int) -> str:
|
||||
client = _get_vision_client()
|
||||
image = vision.Image(content=image_bytes)
|
||||
response = client.document_text_detection(
|
||||
image=image,
|
||||
image_context=vision.ImageContext(language_hints=["he"]),
|
||||
)
|
||||
if response.error.message:
|
||||
raise RuntimeError(f"Vision error page {page_num}: {response.error.message}")
|
||||
text = response.full_text_annotation.text if response.full_text_annotation else ""
|
||||
return _fix_hebrew_quotes(text)
|
||||
|
||||
|
||||
_FOOTER_JUNK_RE = re.compile(
|
||||
r"^("
|
||||
r"\s*|" # blank
|
||||
r"[-·*.\"\'׳״]+|" # stray punctuation
|
||||
r"\d{1,3}[\s\-·*.\"\'׳״]*|" # page number with any stray char
|
||||
r"עמוד[\s\d\-·*.\"\'׳״]*|" # "עמוד" / "עמוד N" w/ trailing noise
|
||||
r"[-·*\s\"\'׳״]*[a-zA-Z][a-zA-Z0-9 .\-·*_]{0,30}" # garbled latin (nevo URL variants)
|
||||
r")$"
|
||||
)
|
||||
|
||||
|
||||
def _clean_page_text(text: str) -> str:
|
||||
"""Strip Nevo page headers, footers and watermarks from a single page's OCR text.
|
||||
|
||||
Nevo footer on each page looks like:
|
||||
עמוד
|
||||
N (or "N·", "N*")
|
||||
nevo.co.il (or OCR-garbled: "new coal", "neto coal", etc.)
|
||||
- (optional stray dash)
|
||||
|
||||
Google Vision OCRs this block at the end of each page's text.
|
||||
"""
|
||||
# 1. Strip top header "עמוד X מתוך Y" anywhere
|
||||
text = PDF_PAGE_HEADER_RE.sub("\n", text)
|
||||
|
||||
# 2. Walk back from end, dropping footer junk lines
|
||||
lines = text.split("\n")
|
||||
while lines and _FOOTER_JUNK_RE.match(lines[-1].strip()):
|
||||
lines.pop()
|
||||
text = "\n".join(lines)
|
||||
|
||||
# 3. Final pass: strip any leftover Nevo URLs mid-text and orphan "עמוד X" lines
|
||||
text = NEVO_URL_RE.sub("", text)
|
||||
text = PDF_PAGE_NUM_LINE_RE.sub("", text)
|
||||
text = PDF_PAGE_ORPHAN_RE.sub("", text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
def proofread_pdf(path: Path) -> tuple[str, dict]:
|
||||
"""Extract clean decision text from Nevo PDF via Google Vision OCR."""
|
||||
doc = fitz.open(str(path))
|
||||
pages: list[str] = []
|
||||
for i, page in enumerate(doc):
|
||||
pix = page.get_pixmap(dpi=300)
|
||||
img_bytes = pix.tobytes("png")
|
||||
text = _ocr_page_image(img_bytes, i + 1)
|
||||
pages.append(_clean_page_text(text))
|
||||
# Small delay between API calls to be safe
|
||||
time.sleep(0.1)
|
||||
doc.close()
|
||||
|
||||
body = "\n\n".join(p for p in pages if p)
|
||||
body = re.sub(r"\n{3,}", "\n\n", body)
|
||||
body = re.sub(r"[ \t]+\n", "\n", body)
|
||||
|
||||
for marker in NEVO_POSTAMBLE_MARKERS:
|
||||
idx = body.find(marker)
|
||||
if idx != -1:
|
||||
body = body[:idx].rstrip()
|
||||
break
|
||||
|
||||
return body, {
|
||||
"pages": len(pages),
|
||||
"chars": len(body),
|
||||
}
|
||||
|
||||
|
||||
# ── Orchestration ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
SKIP_FILES = {
|
||||
"הכנת שאלות מחקר.docx",
|
||||
"סוכן_מנתח_ומחקר_משפטי_Paperclip_מדריך.docx",
|
||||
"README.md",
|
||||
}
|
||||
|
||||
|
||||
def output_filename(src: Path) -> str:
|
||||
"""Build clean output filename preserving case identifier."""
|
||||
stem = src.stem
|
||||
# Normalize: replace spaces with - where helpful, but keep Hebrew intact
|
||||
return f"{stem}.md"
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
RAW_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Filter files
|
||||
only = argv[1:] if len(argv) > 1 else None
|
||||
files: list[Path] = []
|
||||
for p in sorted(TRAINING_DIR.iterdir()):
|
||||
if p.is_dir() or p.name.startswith("."):
|
||||
continue
|
||||
if p.name in SKIP_FILES:
|
||||
continue
|
||||
if p.suffix.lower() not in (".docx", ".pdf"):
|
||||
continue
|
||||
if only and p.name not in only:
|
||||
continue
|
||||
files.append(p)
|
||||
|
||||
print(f"Processing {len(files)} files...\n")
|
||||
|
||||
for path in files:
|
||||
try:
|
||||
if path.suffix.lower() == ".docx":
|
||||
md, stats = proofread_docx(path)
|
||||
else:
|
||||
md, stats = proofread_pdf(path)
|
||||
|
||||
out_path = OUTPUT_DIR / output_filename(path)
|
||||
out_path.write_text(md, encoding="utf-8")
|
||||
print(f"✓ {path.name}")
|
||||
print(f" → {out_path.name} ({len(md):,} chars) {stats}")
|
||||
except Exception as e:
|
||||
print(f"✗ {path.name}: {e}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
254
scripts/seed-appeals.py
Normal file
254
scripts/seed-appeals.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed appeals (cases) from legacy vault metadata."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
APPEALS = [
|
||||
# ── Active (01_Projects) ──
|
||||
{
|
||||
"case_number": "1130/25",
|
||||
"title": "ערר קרית יערים-1 — קובר",
|
||||
"appellants": ["מרק קובר", "יצחק מטמון"],
|
||||
"respondents": ["הוועדה המרחבית הראל", "ליבמן"],
|
||||
"subject": "ערר על אישור תכנית להוספת קומה וזכויות בנייה",
|
||||
"property_address": "רח' אבינדב 23, קריית יערים",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "partial",
|
||||
},
|
||||
{
|
||||
"case_number": "1194/25+1199/25",
|
||||
"title": "ערר קרית יערים-2 — מטמון/קובר",
|
||||
"appellants": ["יצחק מטמון", "מרק קובר"],
|
||||
"respondents": ["הוועדה המקומית"],
|
||||
"subject": "תוספת קומה + הגדלת זכויות בנייה",
|
||||
"property_address": "חלקה 240, גוש 29536, רח' אבינדב",
|
||||
"status": "new",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8027-25",
|
||||
"title": "ערר היטל השבחה תחכמוני 20",
|
||||
"appellants": ["עובדיה", "מירב", "ווינשטיין ואח'"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "היטל השבחה",
|
||||
"property_address": "רח' תחכמוני, ירושלים, גוש 30069, חלקה 156",
|
||||
"status": "new",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
# ── Archived — completed decisions ──
|
||||
{
|
||||
"case_number": "1180-1181",
|
||||
"title": "ערר הכט",
|
||||
"appellants": [],
|
||||
"respondents": [],
|
||||
"subject": "רישוי ובנייה",
|
||||
"property_address": "",
|
||||
"status": "final",
|
||||
"expected_outcome": "rejected",
|
||||
"notes": "פורסם 05.02.2026. דחייה. שימש כמודל לניתוח סגנון.",
|
||||
},
|
||||
{
|
||||
"case_number": "1126/25+1141/25",
|
||||
"title": "תמ\"א 38/2 בית הכרם",
|
||||
"appellants": ["מרכז קהילתי זיו-מרקס", "12 תושבים"],
|
||||
"respondents": ["הוועדה המקומית", "יזם"],
|
||||
"subject": "תמ\"א 38/2 הריסה ובנייה מחדש",
|
||||
"property_address": "רח' החלוץ 36, בית הכרם, גוש 30159/6",
|
||||
"status": "final",
|
||||
"expected_outcome": "partial",
|
||||
"notes": "גרסה סופית טיוטה 9, מרץ 2026. קבלה חלקית. שימש כמודל לניתוח סגנון.",
|
||||
},
|
||||
{
|
||||
"case_number": "8255-25",
|
||||
"title": "בל\"מ אפרים אבי",
|
||||
"appellants": ["אפרים אברהם"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "היטל השבחה — בקשה להארכת מועד",
|
||||
"property_address": "רח' הורקניה 4, קטמונים, ירושלים",
|
||||
"status": "final",
|
||||
"expected_outcome": "rejected",
|
||||
"notes": "גרסה סופית מאושרת. דחייה.",
|
||||
},
|
||||
# ── Archived — unified decisions ──
|
||||
{
|
||||
"case_number": "8107-25",
|
||||
"title": "אבו זאהריה",
|
||||
"appellants": ["אבו זאהריה מפיד"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "ערר על החלטת שמאי מכריע — היטל השבחה",
|
||||
"property_address": "רח' אום כולתום 26, בית חנינא, גוש 30615, חלקה 69",
|
||||
"status": "final",
|
||||
"expected_outcome": "",
|
||||
"notes": "החלטה מאחדת: ערר גפני.",
|
||||
},
|
||||
{
|
||||
"case_number": "9005-24",
|
||||
"title": "רמת שלמה — פיצויים ס' 197",
|
||||
"appellants": ["קירמאיר אסתר ואח' (63-67 עוררים)"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "פיצויים לפי סעיף 197",
|
||||
"property_address": "רמת שלמה, ירושלים, גוש 30561, חלקות 36, 40",
|
||||
"status": "final",
|
||||
"expected_outcome": "",
|
||||
"notes": "החלטה מאחדת: ערר ורדי 9003-23.",
|
||||
},
|
||||
# ── Archived — in progress ──
|
||||
{
|
||||
"case_number": "1113/25",
|
||||
"title": "אייל מבורך לוי ואברהם עדי",
|
||||
"appellants": ["אייל מבורך לוי", "אברהם עדי"],
|
||||
"respondents": ["הוועדה המקומית הראל"],
|
||||
"subject": "הרחבת דירות + תוספת 2 יח\"ד",
|
||||
"property_address": "רח' השלום 63, מבשרת ציון, גוש 30475, חלקה 5",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "1128/25",
|
||||
"title": "שטרית",
|
||||
"appellants": [],
|
||||
"respondents": [],
|
||||
"subject": "",
|
||||
"property_address": "",
|
||||
"status": "drafted",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "1107/06/25",
|
||||
"title": "בלוי נ' הוועדה המקומית",
|
||||
"appellants": ["בלוי מאיר", "מזיע מאיר", "דזימיטרובסקי הדסה"],
|
||||
"respondents": ["הוועדה המקומית ירושלים", "היזם"],
|
||||
"subject": "תוספת בנייה וחיזוק מפני רעידות (תמ\"א 38/1)",
|
||||
"property_address": "רח' הרב בלוי 16, ירושלים, גוש 30099/115",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8141-23",
|
||||
"title": "אזורים בנין",
|
||||
"appellants": ["אזורים בנין (1965) בע\"מ"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "היטל השבחה — תכנית 101-0611905",
|
||||
"property_address": "רח' הנביאים 27, ירושלים",
|
||||
"status": "drafted",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8047-24",
|
||||
"title": "משכן אליהו — היטל השבחה שמאי מכריע",
|
||||
"appellants": ["עומר דרוויש"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "ערר על שמאית מכריעה — היטל השבחה",
|
||||
"property_address": "גוש 30614, חלקה 89, בית חנינא",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "1195-25",
|
||||
"title": "וליד ג'מל",
|
||||
"appellants": ["וליד ג'מל"],
|
||||
"respondents": ["ועדת משנה מטה יהודה", "סמיר מוסא זעאתרה"],
|
||||
"subject": "הסדרת קומה שלישית למשרדים",
|
||||
"property_address": "גוש 30492, חלקה 23, כפר עין נקובא",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "1200/25",
|
||||
"title": "קרית ענבים נופש",
|
||||
"appellants": ["קרית ענבים נופש בע\"מ"],
|
||||
"respondents": ["הוועדה המקומית מטה יהודה", "חברי קיבוץ קרית ענבים"],
|
||||
"subject": "שימוש חורג — סופרמרקט בייעוד ספורט ונופש",
|
||||
"property_address": "קיבוץ קרית ענבים, גוש 29551",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "1184/25",
|
||||
"title": "שטוקהיים — בית נקופה",
|
||||
"appellants": ["אמנון שטוקהיים", "אילנית שטוקהיים"],
|
||||
"respondents": ["הוועדה המקומית מטה יהודה", "יערה טל"],
|
||||
"subject": "אישור בקשה להיתר עם הקלות",
|
||||
"property_address": "מגרש 51, גוש 31399, חלקה 52, בית נקופה",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8070-25",
|
||||
"title": "היטל השבחה — דירת גג",
|
||||
"appellants": ["חיים ראם"],
|
||||
"respondents": ["הוועדה המקומית ירושלים"],
|
||||
"subject": "היטל השבחה — הקלה להשלמת דירת גג",
|
||||
"property_address": "רח' צ.פ. חיות 2, דירה 31, נווה יעקב",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8136-24",
|
||||
"title": "ערר השבחה — מרפסות שירות",
|
||||
"appellants": [],
|
||||
"respondents": [],
|
||||
"subject": "היטל השבחה — מרפסות שירות",
|
||||
"property_address": "",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
{
|
||||
"case_number": "8007-24",
|
||||
"title": "עומר דרוויש — שומה מכרעת",
|
||||
"appellants": [],
|
||||
"respondents": [],
|
||||
"subject": "היטל השבחה",
|
||||
"property_address": "",
|
||||
"status": "in_progress",
|
||||
"expected_outcome": "",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
inserted = 0
|
||||
skipped = 0
|
||||
async with pool.acquire() as conn:
|
||||
for a in APPEALS:
|
||||
existing = await conn.fetchval(
|
||||
"SELECT id FROM cases WHERE case_number = $1", a["case_number"]
|
||||
)
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
await conn.execute(
|
||||
"""INSERT INTO cases
|
||||
(case_number, title, appellants, respondents, subject,
|
||||
property_address, status, expected_outcome, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
|
||||
a["case_number"],
|
||||
a["title"],
|
||||
json.dumps(a.get("appellants", [])),
|
||||
json.dumps(a.get("respondents", [])),
|
||||
a.get("subject", ""),
|
||||
a.get("property_address", ""),
|
||||
a.get("status", "new"),
|
||||
a.get("expected_outcome", ""),
|
||||
a.get("notes", ""),
|
||||
)
|
||||
inserted += 1
|
||||
|
||||
await close_pool()
|
||||
print(f"✓ appeals: {inserted} inserted, {skipped} skipped (already exist)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
449
scripts/seed-knowledge.py
Normal file
449
scripts/seed-knowledge.py
Normal file
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed knowledge tables from legacy vault data.
|
||||
|
||||
Imports: lessons_learned, transition_phrases, case_law, statutory_provisions.
|
||||
Sources: docs/legal-decision-lessons.md, skills/decision/SKILL.md
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add mcp-server to path so we can reuse db module
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Data: Lessons Learned
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
LESSONS = [
|
||||
# --- הכט 1180-1181 (rejected, 02.2026) ---
|
||||
{
|
||||
"lesson_title": "Discussion = continuous essay, no sub-headers",
|
||||
"lesson_text": "הדיון נקרא כחיבור משפטי רציף עם סעיפים ממוספרים, לא כמתווה מובנה עם כותרות משנה. הגרסה המפורסמת של הכט השתמשה באפס כותרות משנה בדיון, בעוד הטיוטה שלנו הכילה 6 כותרות H2.",
|
||||
"category": "structure",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "הכט 1180-1181",
|
||||
"severity": "critical",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Citation through consolidating decision",
|
||||
"lesson_text": "להשתמש בהחלטה מאחדת קודמת (כמו ערר נגאח 1011-03-25) לצטט מספר תקדימים בפסקה אחת ארוכה (~600 מילים), במקום לצטט כל תקדים בפסקה נפרדת.",
|
||||
"category": "style",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "הכט 1180-1181",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Paragraph length variation in discussion",
|
||||
"lesson_text": "לא לפרגמנט טיעונים משפטיים ארוכים לפסקאות זהות וקצרות. לגוון אורך פסקאות מ-20 עד 600+ מילים. פסקאות ציטוט מרכזיות ארוכות מאוד.",
|
||||
"category": "style",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "הכט 1180-1181",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Opening formula promises both conclusion AND elaboration",
|
||||
"lesson_text": 'פתיחת הדיון צריכה להבטיח גם מסקנה וגם הרחבה: "לאחר שבחנו... החלטנו בשלב ראשון כי... אך יחד עם זאת ועל מנת לא לצאת בחסר... מצאנו להוסיף מספר הערות"',
|
||||
"category": "style",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "הכט 1180-1181",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": 'Summary title is "סיכום"',
|
||||
"lesson_text": 'כותרת פרק הסיכום היא "סיכום" בלבד, לא "סיכום והכרעה" ולא "סוף דבר".',
|
||||
"category": "structure",
|
||||
"applies_to": ["block-yod-alef"],
|
||||
"source_case": "הכט 1180-1181",
|
||||
"severity": "nice-to-have",
|
||||
},
|
||||
# --- בית הכרם 1126/25 (partial acceptance, 03.2026) ---
|
||||
{
|
||||
"lesson_title": "Threshold question is STRATEGIC, not mandatory",
|
||||
"lesson_text": "שאלת הסף (זכות ערר לפי ס' 152) היא כלי אסטרטגי, לא חובה. כשלתיק יש שאלות מהותיות חזקות (חניה, קווי בניין, שימור), דפנה מעדיפה להתעמק בתוכן על פני חסימה פרוצדורלית. זה גם מחזק את ההחלטה מפני ביקורת שיפוטית.",
|
||||
"category": "process",
|
||||
"applies_to": ["all"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "critical",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Concentric circles = rejected appeals only",
|
||||
"lesson_text": 'מודל השכבות (עיגולים קונצנטריים, סעיף 6.3 ב-SKILL) הוא כלי אחד מתוך כמה, לא המסגרת הנדרשת. לעררים שמתקבלים חלקית, דפנה משתמשת בניתוח גמיש נושא-נושא.',
|
||||
"category": "process",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "critical",
|
||||
},
|
||||
{
|
||||
"lesson_title": "New opening type: tension mapping",
|
||||
"lesson_text": 'לקבלה חלקית או תיקים עם סוגיות מורכבות מצטלבות, פתיחת "מיפוי מתחים": רשימה של 6+ מתחים ספציפיים בתבליטים לפני הניתוח. דפוס: "בערר דנן עולות שאלות כיצד והאם..." → רשימת מתחים → "כל הנקודות לעיל עומדות לפנינו..."',
|
||||
"category": "structure",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Single building weakens TAMA 38 interest",
|
||||
"lesson_text": 'כשתמ"א 38 חלה על בית בודד (לעומת בניין דירות גדול), אינטרס החיזוק מפני רעידת אדמה חלש יותר. זה מצדיק אישור זהיר יותר של זכויות, במיוחד קווי בניין וחניה.',
|
||||
"category": "content",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Master plan as shield against ad-hoc planning",
|
||||
"lesson_text": 'כשקיימת תכנית אב — לצטט אותה כדי לתת לגיטימציה להיתר בודד. מסקנה: ההיתר "משתלב בחזון כולל קיים" במקום ליצור תקדים אד-הוק.',
|
||||
"category": "content",
|
||||
"applies_to": ["block-yod"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Deep plan provision citations for parking",
|
||||
"lesson_text": "לסוגיות חניה/תשתיות, דפנה נכנסת עמוק להוראות תכנית עם ציטוטים ישירים נרחבים (300+ מילים) וניתוח משולב. כולל מספרי סעיפים ספציפיים (לדוגמה: 6.8(4), 6.8(9), נספח תנועה, 5166b).",
|
||||
"category": "content",
|
||||
"applies_to": ["block-yod", "block-tet"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "important",
|
||||
},
|
||||
{
|
||||
"lesson_title": "Ultra-minimal summary for partial acceptance",
|
||||
"lesson_text": "בקבלה חלקית, כל ההנמקה כבר בדיון. סיכום = הוראות אופרטיביות בלבד (בדרך כלל 3 סעיפים קצרים). ללא דיון בהוצאות. ללא סיום חם.",
|
||||
"category": "structure",
|
||||
"applies_to": ["block-yod-alef"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "important",
|
||||
},
|
||||
# --- קרית יערים-1 (03.2026) ---
|
||||
{
|
||||
"lesson_title": "Neutral background rule",
|
||||
"lesson_text": 'רקע (בלוק ו) = עובדות אובייקטיביות בלבד. מבחן: האם המשפט מכיל ציטוט ישיר מצד, או מילות ערך/שיפוט (חריג, חטא, בעייתי)? אם כן → שייך בטענות (בלוק ז) או דיון (בלוק י), לא ברקע. החלטות קודמות = עובדה יבשה ("ביום X נדחתה תכנית Y"), ללא נימוקים וציטוטים.',
|
||||
"category": "structure",
|
||||
"applies_to": ["block-vav"],
|
||||
"source_case": "קרית יערים-1 (1130/25)",
|
||||
"severity": "critical",
|
||||
},
|
||||
{
|
||||
"lesson_title": "12-block mandatory structure",
|
||||
"lesson_text": 'מבנה 12 בלוקים פורמלי חובה עם שלב "טיוטת טרום-דיון". כולל: פתיחה (ה) → רקע (ו) → טענות (ז) → הליכים (ח) → תכניות (ט) → דיון (י) → סיכום (יא). חידוש מאריאלי: "ההליכים בפני ועדת הערר" כפרק נפרד. כל בלוק נכתב כאילו שופט בית משפט מנהלי קורא בפעם הראשונה.',
|
||||
"category": "structure",
|
||||
"applies_to": ["all"],
|
||||
"source_case": "קרית יערים-1 (1130/25)",
|
||||
"severity": "critical",
|
||||
},
|
||||
# --- Meta-lesson ---
|
||||
{
|
||||
"lesson_title": "Skill was over-indexed on single case type",
|
||||
"lesson_text": "ה-SKILL המקורי היה מבוסס יתר על מקרה אחד (הכט = דחייה). מודל העיגולים, שאלת סף כחובה, וסיום חם — כולם דפוסים מתיק בודד. בית הכרם (קבלה חלקית) חשף שהגישה של דפנה גמישה יותר ממה שתפסנו. צריך להבחין בין דפוסים אוניברסליים לתלויי-תוצאה.",
|
||||
"category": "process",
|
||||
"applies_to": ["all"],
|
||||
"source_case": "בית הכרם 1126/25",
|
||||
"severity": "critical",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Data: Transition Phrases
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
TRANSITION_PHRASES = [
|
||||
# From הכט
|
||||
{"phrase": "ועל מנת לא לצאת בחסר", "usage_context": "פתיחת אוביטר דיקטה / הנמקה נוספת", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "נציין כי טענות אלו נטענו בלשון רפה", "usage_context": "הכרה בטענות חלשות תוך דיון בהן", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "עינינו הרואות", "usage_context": "סיכום אחרי ציטוט ארוך", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "נוסיף.", "usage_context": "מעבר קצר ביותר (מילה אחת) לנקודה הבאה", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "אם כך, לעת הזו", "usage_context": "הסקת מסקנה מציטוטים", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "למעלה מן הצורך", "usage_context": "דיון לא הכרחי להכרעה אך נכתב מטעמים אסטרטגיים", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "למיטב הבנתנו", "usage_context": "עמדה זהירה בשאלה משפטית לא מיושבת", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
{"phrase": "נשלים ונציין", "usage_context": "נקודה אחרונה לפני מעבר לסיכום", "block_types": ["block-yod"], "source_decision": "הכט 1180-1181"},
|
||||
# From בית הכרם
|
||||
{"phrase": "הדברים משליכים על שיקול הדעת ב...", "usage_context": "קישור ממצא למסקנה", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
{"phrase": "רוצה לומר כי", "usage_context": "ניסוח חלופי / הסבר", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
{"phrase": "נוצר מצב בו", "usage_context": "הצגת מצב עובדתי / בעיה", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
{"phrase": "לכך נוסיף כי", "usage_context": "הוספת שכבה נוספת לטיעון", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
{"phrase": "יש אולי להצר על כך ש...", "usage_context": "הערה ביקורתית עדינה (כלפי רשות תכנון)", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
{"phrase": "עם ההבנה לטענה זו של העוררים, אין בידנו לקבלה", "usage_context": "הכרה רכה בטענה תוך דחייתה", "block_types": ["block-yod"], "source_decision": "בית הכרם 1126/25"},
|
||||
# General (from SKILL.md)
|
||||
{"phrase": "ברי כי", "usage_context": "מסקנה מובנת מאליה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נפנה ל...", "usage_context": "פתיחת ניתוח חוק/פסיקה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "מכל האמור לעיל", "usage_context": "מעבר לסיכום", "block_types": ["block-yod", "block-yod-alef"], "source_decision": ""},
|
||||
{"phrase": "נשוב על כך כי", "usage_context": "חזרה מכוונת על עיקרון חשוב", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "דא עקא", "usage_context": "הצגת בעיה מרכזית או סתירה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "ובמילים אחרות", "usage_context": "הבהרה / ניסוח מחדש", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "הגענו לכלל מסקנה כי", "usage_context": "מסקנה מרכזית (פתיחת דיון)", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "לא נוכל לקבל", "usage_context": "דחיית עמדה / טענה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "מקובלת עלינו", "usage_context": "קבלת עמדה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "התרשמנו כי", "usage_context": "מסקנה מדיון / עיון במסמכים", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נחדד כי", "usage_context": "חידוד נקודה קודמת", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "סיכומם של דברים", "usage_context": "פתיחת סיכום מהותי לפני פרק הסיכום", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "המסקנה מכל האמור היא כי", "usage_context": "מסקנת ביניים מקיפה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "לעמדתנו", "usage_context": "עמדת הוועדה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "בנסיבות אלה", "usage_context": "מעבר מעובדות למסקנה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נזכיר כי", "usage_context": "תזכורת לעיקרון ידוע", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "מצאנו כי", "usage_context": "קביעה עובדתית", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "שוכנענו כי", "usage_context": "קביעה לאחר בחינה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "על כן ולו רק מסיבה זו", "usage_context": "נטרול טענה חלשה לפני ניתוח עמוק", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "יחד עם זאת, מצאנו לנכון לדון בשאלה העקרונית", "usage_context": "מעבר לדיון עקרוני למרות דחייה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "משכך", "usage_context": "הסקת מסקנה מעמדה שהוצגה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "הדברים מתחדדים שעה ש...", "usage_context": "הבהרה נוספת לאור נסיבות", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "זאת ועוד", "usage_context": "הוספת נימוק", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "יתרה מכך", "usage_context": "חיזוק הנמקה קודמת", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "לאור כל האמור לעיל", "usage_context": "פתיחת סיכום סופי", "block_types": ["block-yod", "block-yod-alef"], "source_decision": ""},
|
||||
{"phrase": "נפתח בכך כי", "usage_context": "פתיחת דיון (לא מסמך)", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נפנה בעניין זה להחלטת...", "usage_context": "הפניה לתקדים", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "ברי כי משאב הקרקע יקר לבעליו ולציבור", "usage_context": "הצדקת שימוש יעיל בקרקע", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נסכם כי", "usage_context": "מעבר לסיכום ביניים", "block_types": ["block-yod"], "source_decision": ""},
|
||||
{"phrase": "נחזור על כך כי", "usage_context": "חזרה אמפתית על קביעה חשובה", "block_types": ["block-yod"], "source_decision": ""},
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Data: Case Law
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
CASE_LAW = [
|
||||
{
|
||||
"case_number": "עע\"מ 3975/22",
|
||||
"case_name": "ב. קרן-נכסים",
|
||||
"court": "בית המשפט העליון",
|
||||
"subject_tags": ["proprietary_claims", "feasibility"],
|
||||
"summary": "פסק דין מנחה בנושא בדיקת היתכנות קניינית — מתי ועדה צריכה לבחון זכויות קניין לפני מתן היתר.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר (מרכז) 1011-03-25",
|
||||
"case_name": "נגאח עבד אל קאדר",
|
||||
"court": "ועדת ערר מרכז",
|
||||
"subject_tags": ["proprietary_claims", "consolidating_decision"],
|
||||
"summary": "החלטה מאחדת בנושא טענות קנייניות — ריכזה את כל הפסיקה בנושא.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר 1071/25",
|
||||
"case_name": "מינץ",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["self_reference", "previous_decision"],
|
||||
"summary": "החלטה קודמת של ועדת הערר עצמה — שימוש כתקדים פנימי.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר 1192/18",
|
||||
"case_name": "אילן",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["preservation", "nuisance"],
|
||||
"summary": "שימור ומטרדים — איזון בין שימור מבנים לזכויות שכנים.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר 1009-02-24",
|
||||
"case_name": "מובשוביץ",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["urban_renewal", "tama_38"],
|
||||
"summary": 'התחדשות עירונית — ציטוט נרחב (~400 מילים) בהחלטת בית הכרם.',
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר 1156/18",
|
||||
"case_name": "ארד",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["construction_nuisance"],
|
||||
"summary": "מטרדי בנייה — מתי מטרד בנייה מצדיק התערבות.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר 1169/19",
|
||||
"case_name": "זוהר",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["construction_nuisance"],
|
||||
"summary": "מטרדי בנייה — המשך קו הפסיקה של ערר ארד.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר (ירושלים) 1078+1083/24",
|
||||
"case_name": "אריאלי",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["structure_example", "proceedings_block"],
|
||||
"summary": "שימשה כמודל מבני — פרק הליכים נפרד (31 סעיפים), מבנה מפורט.",
|
||||
"key_quote": "",
|
||||
},
|
||||
{
|
||||
"case_number": "ערר אדלר",
|
||||
"case_name": "אדלר",
|
||||
"court": "ועדת ערר ירושלים",
|
||||
"subject_tags": ["consolidating_decision"],
|
||||
"summary": "החלטה מאחדת שצוטטה בבית הכרם — טכניקת ציטוט דרך החלטה מרכזת.",
|
||||
"key_quote": "",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Data: Statutory Provisions
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
STATUTORY_PROVISIONS = [
|
||||
{
|
||||
"statute_name": "חוק התכנון והבנייה, תשכ\"ה-1965",
|
||||
"section_number": "152(א)(2)",
|
||||
"section_title": "זכות ערר על אישור תכנית",
|
||||
"full_text": "",
|
||||
"common_usage": "שאלת סף — האם קיימת זכות ערר. כלי אסטרטגי, לא חובה.",
|
||||
"subject_tags": ["threshold", "right_to_appeal"],
|
||||
},
|
||||
{
|
||||
"statute_name": "חוק התכנון והבנייה, תשכ\"ה-1965",
|
||||
"section_number": "149",
|
||||
"section_title": "הקלה",
|
||||
"full_text": "",
|
||||
"common_usage": "בקשות להקלה — סטייה מתכנית בניין עיר.",
|
||||
"subject_tags": ["deviation", "relief"],
|
||||
},
|
||||
{
|
||||
"statute_name": "חוק התכנון והבנייה, תשכ\"ה-1965",
|
||||
"section_number": "145",
|
||||
"section_title": "היתר בנייה",
|
||||
"full_text": "",
|
||||
"common_usage": "עררים על סירוב/אישור היתר בנייה.",
|
||||
"subject_tags": ["building_permit"],
|
||||
},
|
||||
{
|
||||
"statute_name": "חוק התכנון והבנייה, תשכ\"ה-1965",
|
||||
"section_number": "196-198",
|
||||
"section_title": "היטל השבחה",
|
||||
"full_text": "",
|
||||
"common_usage": "עררי היטל השבחה (8xxx) — חיוב בגין עליית שווי מקרקעין.",
|
||||
"subject_tags": ["betterment_levy"],
|
||||
},
|
||||
{
|
||||
"statute_name": "חוק התכנון והבנייה, תשכ\"ה-1965",
|
||||
"section_number": "197",
|
||||
"section_title": "פיצויים בגין ירידת ערך",
|
||||
"full_text": "",
|
||||
"common_usage": "עררי פיצויים (9xxx) — תביעה בגין ירידת ערך מקרקעין בשל תכנית.",
|
||||
"subject_tags": ["compensation", "depreciation"],
|
||||
},
|
||||
{
|
||||
"statute_name": "תמ\"א 38",
|
||||
"section_number": "תיקון 2 + 3",
|
||||
"section_title": "חיזוק מבנים מפני רעידות אדמה",
|
||||
"full_text": "",
|
||||
"common_usage": "חיזוק/הריסה ובנייה מחדש. אינטרס חלש יותר בבית בודד.",
|
||||
"subject_tags": ["tama_38", "seismic_reinforcement"],
|
||||
},
|
||||
{
|
||||
"statute_name": "חוק המקרקעין, תשכ\"ט-1969",
|
||||
"section_number": "71ב(א)(1)",
|
||||
"section_title": "רוב הדרוש לשינוי ברכוש משותף",
|
||||
"full_text": "",
|
||||
"common_usage": "בדיקת היתכנות קניינית — האם יש רוב לשינוי ברכוש משותף.",
|
||||
"subject_tags": ["proprietary_claims", "common_property"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Import Logic
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
async def seed_lessons(conn) -> int:
|
||||
count = 0
|
||||
for l in LESSONS:
|
||||
existing = await conn.fetchval(
|
||||
"SELECT id FROM lessons_learned WHERE lesson_title = $1", l["lesson_title"]
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
await conn.execute(
|
||||
"""INSERT INTO lessons_learned (lesson_title, lesson_text, category, applies_to, source_case, severity)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
l["lesson_title"], l["lesson_text"], l["category"],
|
||||
json.dumps(l["applies_to"]), l["source_case"], l["severity"],
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def seed_phrases(conn) -> int:
|
||||
count = 0
|
||||
for p in TRANSITION_PHRASES:
|
||||
existing = await conn.fetchval(
|
||||
"SELECT id FROM transition_phrases WHERE phrase = $1", p["phrase"]
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
await conn.execute(
|
||||
"""INSERT INTO transition_phrases (phrase, usage_context, block_types, source_decision)
|
||||
VALUES ($1, $2, $3, $4)""",
|
||||
p["phrase"], p["usage_context"],
|
||||
json.dumps(p["block_types"]), p["source_decision"],
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def seed_case_law(conn) -> int:
|
||||
count = 0
|
||||
for c in CASE_LAW:
|
||||
existing = await conn.fetchval(
|
||||
"SELECT id FROM case_law WHERE case_number = $1", c["case_number"]
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
await conn.execute(
|
||||
"""INSERT INTO case_law (case_number, case_name, court, subject_tags, summary, key_quote)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
c["case_number"], c["case_name"], c["court"],
|
||||
json.dumps(c["subject_tags"]), c["summary"], c.get("key_quote", ""),
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def seed_statutes(conn) -> int:
|
||||
count = 0
|
||||
for s in STATUTORY_PROVISIONS:
|
||||
existing = await conn.fetchval(
|
||||
"""SELECT id FROM statutory_provisions
|
||||
WHERE statute_name = $1 AND section_number = $2""",
|
||||
s["statute_name"], s["section_number"],
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
await conn.execute(
|
||||
"""INSERT INTO statutory_provisions
|
||||
(statute_name, section_number, section_title, full_text, common_usage, subject_tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
s["statute_name"], s["section_number"], s["section_title"],
|
||||
s["full_text"], s["common_usage"], json.dumps(s["subject_tags"]),
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
n_lessons = await seed_lessons(conn)
|
||||
n_phrases = await seed_phrases(conn)
|
||||
n_case_law = await seed_case_law(conn)
|
||||
n_statutes = await seed_statutes(conn)
|
||||
|
||||
await close_pool()
|
||||
|
||||
print(f"✓ lessons_learned: {n_lessons} inserted")
|
||||
print(f"✓ transition_phrases: {n_phrases} inserted")
|
||||
print(f"✓ case_law: {n_case_law} inserted")
|
||||
print(f"✓ statutory_provisions: {n_statutes} inserted")
|
||||
print(f" Total: {n_lessons + n_phrases + n_case_law + n_statutes} records")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
40
scripts/test-search.py
Normal file
40
scripts/test-search.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test semantic search functions."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import search_similar_paragraphs, search_similar_case_law, search_precedents, init_schema
|
||||
from legal_mcp.services.embeddings import embed_query
|
||||
|
||||
|
||||
async def main():
|
||||
await init_schema()
|
||||
|
||||
queries = [
|
||||
"טענות קנייניות רוב דרוש בעלי דירות רכוש משותף",
|
||||
"חניה תנועה חניות מצוקת חניה",
|
||||
"היטל השבחה שמאי מכריע התערבות",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f'=== שאילתה: "{query}" ===')
|
||||
emb = await embed_query(query)
|
||||
results = await search_precedents(emb, limit=3)
|
||||
|
||||
if not results:
|
||||
print(" אין תוצאות")
|
||||
else:
|
||||
for i, r in enumerate(results):
|
||||
score = r["score"]
|
||||
cn = r["case_number"]
|
||||
rtype = r["type"]
|
||||
content = r["content"][:120].replace("\n", " ")
|
||||
print(f" {i+1}. [{rtype}] {score:.3f} | {cn} | {content}")
|
||||
print()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
257
scripts/validate-decision.py
Normal file
257
scripts/validate-decision.py
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a decision against block-schema rules.
|
||||
|
||||
Usage: python validate-decision.py <case_number>
|
||||
|
||||
Checks:
|
||||
1. Neutral background (block-vav) — no party quotes or value words
|
||||
2. Weight compliance — blocks within expected ranges
|
||||
3. Structural integrity — all required blocks present
|
||||
4. Claims coverage — every claim in block-zayin addressed in block-yod
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
|
||||
|
||||
from legal_mcp.services.db import get_pool, init_schema, close_pool
|
||||
|
||||
|
||||
# Value/judgment words that shouldn't appear in neutral background
|
||||
VALUE_WORDS = [
|
||||
"חריג", "חטא", "בעייתי", "מזעזע", "שערורייתי", "מגוחך",
|
||||
"נפשע", "פגום", "חמור", "מקומם", "בלתי סביר", "מופרז",
|
||||
"מגונה", "פסול", "נלוז", "מטריד",
|
||||
]
|
||||
|
||||
# Party quote indicators
|
||||
QUOTE_INDICATORS = [
|
||||
r"לטענת\s+(העוררי|המשיב|מבקשי)",
|
||||
r"לדברי\s+(העוררי|המשיב|מבקשי)",
|
||||
r"העורר\s+טוען",
|
||||
r"המשיבה\s+טוענת",
|
||||
r"לשיטת\s+(העוררי|המשיב)",
|
||||
]
|
||||
|
||||
# Expected weight ranges per block type (for רישוי appeals)
|
||||
WEIGHT_RANGES_LICENSING = {
|
||||
"block-he": (0.5, 5),
|
||||
"block-vav": (3, 40),
|
||||
"block-zayin": (13, 40),
|
||||
"block-chet": (0, 15),
|
||||
"block-tet": (0, 15),
|
||||
"block-yod": (30, 75),
|
||||
"block-yod-alef": (1, 10),
|
||||
"block-yod-bet": (0, 2),
|
||||
}
|
||||
|
||||
# Expected weight ranges for היטל השבחה
|
||||
WEIGHT_RANGES_LEVY = {
|
||||
"block-he": (0, 5),
|
||||
"block-vav": (2, 20),
|
||||
"block-zayin": (15, 40),
|
||||
"block-chet": (0, 25),
|
||||
"block-tet": (0, 15),
|
||||
"block-yod": (25, 75),
|
||||
"block-yod-alef": (1, 10),
|
||||
"block-yod-bet": (0, 3),
|
||||
}
|
||||
|
||||
|
||||
def check_neutral_background(content: str) -> list[str]:
|
||||
"""Check block-vav for neutrality violations."""
|
||||
issues = []
|
||||
if not content:
|
||||
return issues
|
||||
|
||||
lines = content.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
# Check value words
|
||||
for word in VALUE_WORDS:
|
||||
if word in line:
|
||||
issues.append(f"מילת שיפוט ברקע (שורה {i+1}): \"{word}\" — \"{line[:80]}...\"")
|
||||
|
||||
# Check party quotes
|
||||
for pattern in QUOTE_INDICATORS:
|
||||
if re.search(pattern, line):
|
||||
match = re.search(pattern, line).group()
|
||||
issues.append(f"ציטוט מצד ברקע (שורה {i+1}): \"{match}\" — \"{line[:80]}...\"")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def check_weight_compliance(blocks: list[dict], appeal_type: str) -> list[str]:
|
||||
"""Check block weights are within expected ranges."""
|
||||
issues = []
|
||||
ranges = WEIGHT_RANGES_LEVY if appeal_type == "levy" else WEIGHT_RANGES_LICENSING
|
||||
|
||||
total_words = sum(b["word_count"] for b in blocks)
|
||||
if total_words == 0:
|
||||
return ["אין תוכן בהחלטה"]
|
||||
|
||||
for block in blocks:
|
||||
bid = block["block_id"]
|
||||
if bid in ranges and block["word_count"] > 0:
|
||||
weight = block["word_count"] / total_words * 100
|
||||
low, high = ranges[bid]
|
||||
if weight < low:
|
||||
issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מתחת לטווח ({low}-{high}%)")
|
||||
elif weight > high:
|
||||
issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מעל לטווח ({low}-{high}%)")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def check_structural_integrity(blocks: list[dict]) -> list[str]:
|
||||
"""Check all required blocks are present."""
|
||||
issues = []
|
||||
required = ["block-he", "block-zayin", "block-yod"]
|
||||
block_ids = {b["block_id"] for b in blocks if b["word_count"] > 0}
|
||||
|
||||
for req in required:
|
||||
if req not in block_ids:
|
||||
issues.append(f"בלוק חובה חסר: {req}")
|
||||
|
||||
# Check discussion is the heaviest block
|
||||
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
|
||||
if yod:
|
||||
max_block = max((b for b in blocks if b["block_id"] not in ("block-alef", "block-bet", "block-gimel", "block-dalet")),
|
||||
key=lambda x: x["word_count"], default=None)
|
||||
if max_block and max_block["block_id"] != "block-yod":
|
||||
issues.append(f"בלוק הדיון (י) אינו הבלוק הגדול ביותר — {max_block['title']} ({max_block['word_count']} מילים) גדול יותר")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def check_no_duplication(vav_content: str, yod_content: str) -> list[str]:
|
||||
"""Check block-yod doesn't repeat block-vav content."""
|
||||
issues = []
|
||||
if not vav_content or not yod_content:
|
||||
return issues
|
||||
|
||||
# Find sentences from background that appear verbatim in discussion
|
||||
vav_sentences = [s.strip() for s in re.split(r'[.!?]', vav_content) if len(s.strip()) > 30]
|
||||
for sent in vav_sentences:
|
||||
if sent in yod_content:
|
||||
issues.append(f"כפילות: משפט מהרקע חוזר בדיון — \"{sent[:60]}...\"")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("שימוש: python validate-decision.py <מספר_תיק>")
|
||||
sys.exit(1)
|
||||
|
||||
case_number = sys.argv[1]
|
||||
await init_schema()
|
||||
pool = await get_pool()
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
case = await conn.fetchrow(
|
||||
"SELECT * FROM cases WHERE case_number = $1", case_number
|
||||
)
|
||||
if not case:
|
||||
print(f"תיק {case_number} לא נמצא")
|
||||
sys.exit(1)
|
||||
|
||||
decision = await conn.fetchrow(
|
||||
"SELECT * FROM decisions WHERE case_id = $1",
|
||||
case["id"],
|
||||
)
|
||||
if not decision:
|
||||
print(f"אין החלטה לתיק {case_number}")
|
||||
sys.exit(1)
|
||||
|
||||
blocks = await conn.fetch(
|
||||
"""SELECT block_id, title, content, word_count, weight_percent
|
||||
FROM decision_blocks WHERE decision_id = $1
|
||||
ORDER BY block_index""",
|
||||
decision["id"],
|
||||
)
|
||||
blocks = [dict(b) for b in blocks]
|
||||
|
||||
claims_count = await conn.fetchval(
|
||||
"SELECT count(*) FROM claims WHERE case_id = $1", case["id"]
|
||||
)
|
||||
|
||||
await close_pool()
|
||||
|
||||
# Determine appeal type
|
||||
num = case_number.split("/")[0].split("+")[0].split("-")[0]
|
||||
if num.startswith("8"):
|
||||
appeal_type = "levy"
|
||||
appeal_type_heb = "היטל השבחה"
|
||||
elif num.startswith("9"):
|
||||
appeal_type = "compensation"
|
||||
appeal_type_heb = "פיצויים"
|
||||
else:
|
||||
appeal_type = "licensing"
|
||||
appeal_type_heb = "רישוי ובנייה"
|
||||
|
||||
print(f"{'='*60}")
|
||||
print(f"ולידציה: {case_number} — {case['title']}")
|
||||
print(f"סוג: {appeal_type_heb} | מילים: {decision['total_words']} | טענות: {claims_count}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
all_issues = []
|
||||
|
||||
# 1. Neutral background
|
||||
vav = next((b for b in blocks if b["block_id"] == "block-vav"), None)
|
||||
issues = check_neutral_background(vav["content"] if vav else "")
|
||||
if issues:
|
||||
print(f"\n❌ רקע ניטרלי — {len(issues)} בעיות:")
|
||||
for i in issues:
|
||||
print(f" • {i}")
|
||||
all_issues.extend(issues)
|
||||
else:
|
||||
print("\n✅ רקע ניטרלי — תקין")
|
||||
|
||||
# 2. Weight compliance
|
||||
issues = check_weight_compliance(blocks, appeal_type)
|
||||
if issues:
|
||||
print(f"\n⚠ משקלות — {len(issues)} חריגות:")
|
||||
for i in issues:
|
||||
print(f" • {i}")
|
||||
all_issues.extend(issues)
|
||||
else:
|
||||
print("\n✅ משקלות — בטווח")
|
||||
|
||||
# 3. Structural integrity
|
||||
issues = check_structural_integrity(blocks)
|
||||
if issues:
|
||||
print(f"\n❌ מבנה — {len(issues)} בעיות:")
|
||||
for i in issues:
|
||||
print(f" • {i}")
|
||||
all_issues.extend(issues)
|
||||
else:
|
||||
print("\n✅ מבנה — תקין")
|
||||
|
||||
# 4. No duplication
|
||||
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
|
||||
issues = check_no_duplication(
|
||||
vav["content"] if vav else "",
|
||||
yod["content"] if yod else "",
|
||||
)
|
||||
if issues:
|
||||
print(f"\n⚠ כפילויות — {len(issues)} נמצאו:")
|
||||
for i in issues:
|
||||
print(f" • {i}")
|
||||
all_issues.extend(issues)
|
||||
else:
|
||||
print("\n✅ ללא כפילויות — תקין")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
if all_issues:
|
||||
print(f"סה\"כ: {len(all_issues)} בעיות נמצאו")
|
||||
else:
|
||||
print("✅ ההחלטה עומדת בכל הכללים")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
264
skills/assistant/SKILL.md
Normal file
264
skills/assistant/SKILL.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
name: legal-assistant
|
||||
description: עוזר משפטי לניתוח וקטלוג תיקים שיפוטיים. Skill זה משמש לריכוז, ניתוח וקטלוג כרונולוגי של מסמכים משפטיים סרוקים ליצירת ציר זמן דיוני (Table of Contents). יש להשתמש ב-skill זה כאשר המשתמש מבקש לנתח מסמכים משפטיים, ליצור טבלת תוכן עניינים לתיק, או להכין תשתית לכתיבת פסק דין.
|
||||
---
|
||||
|
||||
# עוזר משפטי לניהול וקטלוג תיק שיפוטי
|
||||
|
||||
## מטרת ה-Skill
|
||||
|
||||
ריכוז, ניתוח וקטלוג כרונולוגי של מאגר מסמכים סרוקים לצורך הכנת תשתית לכתיבת פסק דין.
|
||||
התוצר הסופי הוא "ציר זמן דיוני" מפורט ומדויק בפורמט טבלה.
|
||||
|
||||
## מתי להשתמש ב-Skill
|
||||
|
||||
- כאשר המשתמש מעלה מסמכים משפטיים לניתוח
|
||||
- כאשר נדרש ליצור ציר זמן דיוני או Table of Contents
|
||||
- כאשר נדרש לסכם ולקטלג תיק שיפוטי
|
||||
- כאשר מכינים תשתית לכתיבת פסק דין
|
||||
|
||||
## תהליך העבודה
|
||||
|
||||
### שלב א': הכנת החומר (Preprocessing)
|
||||
|
||||
1. **קיבוץ מסמכים**: לעבוד על קבוצות של עד 20-30 קבצים בכל פעם כדי לא להעמיס על זיכרון ההקשר.
|
||||
2. **שמות קבצים**: לוודא שלקבצים יש שמות בסיסיים (למשל: `doc_001.pdf`).
|
||||
3. **תמלול PDF ל-Markdown** ⚠️ **חובה**:
|
||||
- **לכל קובץ PDF יש ליצור קובץ MD מקביל** באותה תיקייה
|
||||
- להשתמש בכלי `mcp__gemini-vision__analyze_document` לתמלול
|
||||
- לשמור את התמלול עם אותו שם קובץ וסיומת `.md`
|
||||
- **דוגמה**: `כתב-ערר.pdf` → `כתב-ערר.md`
|
||||
- לשמור על מבנה המסמך המקורי: כותרות, פסקאות, רשימות ממוספרות, טבלאות
|
||||
|
||||
### שלב ב': ניתוח המסמכים
|
||||
|
||||
עבור כל מסמך, לבצע את הפעולות הבאות:
|
||||
|
||||
1. **זיהוי תאריך קובע**:
|
||||
- לחפש חותמת "נתקבל" של בית המשפט - זהו התאריך הקובע
|
||||
- אם אין חותמת, לחפש את תאריך המכתב/בקשה
|
||||
- **חשוב**: להבחין בין תאריך יצירת המסמך לבין תאריך הגשתו
|
||||
|
||||
2. **זיהוי הצד המגיש**:
|
||||
- תובע / נתבע / צד ג' / בית המשפט (החלטה)
|
||||
|
||||
3. **סיכום המהות**:
|
||||
- לסכם ב-20 מילים את מהות המסמך והסעד המבוקש
|
||||
|
||||
4. **הערות מיוחדות**:
|
||||
- אם המסמך הוא "תגובה" המציינת תאריך של מסמך קודם - לציין זאת
|
||||
- לציין חריגות: "הוגש באיחור", "חתימה חסרה" וכו'
|
||||
|
||||
### שלב ג': מבנה הטבלה הנדרש
|
||||
|
||||
הטבלה צריכה לכלול את העמודות הבאות:
|
||||
|
||||
| עמודה | תיאור |
|
||||
|-------|-------|
|
||||
| מספר סידורי | לפי סדר כרונולוגי |
|
||||
| תאריך קובע | תאריך ההגשה/חותמת |
|
||||
| שם המסמך | תיאור משפטי תקני |
|
||||
| הצד המגיש | תובע/נתבע/צד ג'/בית המשפט |
|
||||
| תמצית המהות | 2-3 משפטים על עיקרי הטענות/ההחלטה |
|
||||
| הערות מיוחדות | חריגות, קישורים למסמכים קודמים |
|
||||
|
||||
### פורמט הפלט
|
||||
|
||||
להציג את התוצאה בפורמט טבלה מוכנה להעתקה ל-Excel, לדוגמה:
|
||||
|
||||
```
|
||||
מס' | תאריך | שם המסמך | מגיש | תמצית | הערות
|
||||
1 | 01/01/2024 | כתב תביעה | תובע | תביעה לפיצויים בסך 100,000 ש"ח בגין הפרת חוזה | -
|
||||
2 | 15/01/2024 | כתב הגנה | נתבע | הכחשת טענות התובע, טענה לפגם בחוזה | תגובה לכתב תביעה מ-01/01/2024
|
||||
```
|
||||
|
||||
## נקודות קריטיות לבקרת איכות
|
||||
|
||||
1. **פענוח חותמות**: חותמות דיו חלשות עלולות להיות קשות לקריאה - לבקש מהמשתמש לאמת במקרה של ספק.
|
||||
|
||||
2. **הבחנה בין סוגי מסמכים**: להקפיד לא להתבלבל בין:
|
||||
- "החלטה" לבין "בקשה"
|
||||
- "פסק דין" לבין "החלטה"
|
||||
- "תגובה" לבין "בקשה עצמאית"
|
||||
|
||||
3. **הצלבת נתונים**: כאשר מסמך מתייחס למסמך קודם - לציין את הקשר בעמודת ההערות.
|
||||
|
||||
## דוגמה לפרומפט עבודה
|
||||
|
||||
כאשר המשתמש מעלה מסמכים, להגיב בצורה הבאה:
|
||||
|
||||
> אני מנתח את המסמכים שהעלית. עבור כל מסמך אזהה:
|
||||
> - תאריך קובע (חותמת "נתקבל" או תאריך המסמך)
|
||||
> - הצד המגיש
|
||||
> - מהות המסמך
|
||||
>
|
||||
> אציג את התוצאות בטבלה מוכנה להעתקה ל-Excel.
|
||||
> האם יש מסמכים נוספים לניתוח?
|
||||
|
||||
---
|
||||
|
||||
## טיפול בעררים/תיקים מאוחדים
|
||||
|
||||
כאשר מטפלים במספר עררים או תיקים שאוחדו לדיון משותף:
|
||||
|
||||
### מבנה תיקיות מותאם
|
||||
|
||||
```
|
||||
[שם-פרויקט]/
|
||||
├── README.md # סיכום התיק
|
||||
├── קטלוג-מסמכים.md # ציר זמן דיוני
|
||||
├── החלטה/ # לכתיבת ההחלטה
|
||||
├── תכנון/ # טיוטות
|
||||
└── חומרי-מקור/
|
||||
├── [מספר-ערר-1]/ # מסמכים ייחודיים לערר 1
|
||||
│ └── כתב-ערר/
|
||||
├── [מספר-ערר-2]/ # מסמכים ייחודיים לערר 2
|
||||
│ └── כתב-ערר/
|
||||
└── משותף/ # מסמכים משותפים לכל העררים
|
||||
├── כתבי-תשובה/
|
||||
├── פרוטוקולי-דיון/
|
||||
├── החלטות/
|
||||
├── בקשות/
|
||||
├── חוות-דעת/
|
||||
└── מסמכים-נוספים/
|
||||
```
|
||||
|
||||
### סיווג מסמכים
|
||||
|
||||
| סוג מסמך | תיקיית יעד |
|
||||
|----------|------------|
|
||||
| כתבי ערר ייחודיים | `[מספר-ערר]/כתב-ערר/` |
|
||||
| כתבי תשובה | `משותף/כתבי-תשובה/` |
|
||||
| פרוטוקולי דיון | `משותף/פרוטוקולי-דיון/` |
|
||||
| החלטות ביניים/סופיות | `משותף/החלטות/` |
|
||||
| בקשות (דחייה, הארכה) | `משותף/בקשות/` |
|
||||
| חוות דעת מומחה | `משותף/חוות-דעת/` |
|
||||
| מצגות, תשריטים, אחר | `משותף/מסמכים-נוספים/` |
|
||||
|
||||
---
|
||||
|
||||
## שינוי שמות קבצים
|
||||
|
||||
### פורמט שמות
|
||||
|
||||
```
|
||||
[YYYY-MM-DD]-[סוג-מסמך]-[מגיש]-[פרטים].pdf
|
||||
```
|
||||
|
||||
### דוגמאות
|
||||
|
||||
| שם מקורי | שם חדש |
|
||||
|----------|--------|
|
||||
| `ערר.pdf` | `2025-02-09-כתב-ערר-רחמים-ואחרים.pdf` |
|
||||
| `כתב תשובה לעררים...pdf` | `2025-06-16-כתב-תשובה-ועדה-מקומית.pdf` |
|
||||
| `פרוטוקול דיון...pdf` | `2025-06-29-פרוטוקול-דיון-ראשון.pdf` |
|
||||
| `בקשה דחופה...pdf` | `2025-07-16-בקשה-דחיית-דיון-יציב.pdf` |
|
||||
|
||||
### כללי שמות
|
||||
|
||||
1. **תאריך בהתחלה** - מאפשר מיון כרונולוגי אוטומטי
|
||||
2. **מקפים במקום רווחים** - מונע בעיות בטרמינל
|
||||
3. **עברית מותרת** - לקריאות מיטבית
|
||||
4. **ללא תווים מיוחדים** - רק אותיות, מספרים, מקפים
|
||||
|
||||
---
|
||||
|
||||
## זיהוי וטיפול בכפילויות
|
||||
|
||||
### סוגי כפילויות נפוצים
|
||||
|
||||
1. **signed vs unsigned**:
|
||||
- לשמור רק גרסת `signed`
|
||||
- לסמן unsigned כ-`-מסמך-כפול` או למחוק
|
||||
|
||||
2. **קבצים זהים בתיקיות שונות** (בעררים מאוחדים):
|
||||
- להעביר עותק אחד ל-`משותף/`
|
||||
- לא לשכפל לכל תיקיית ערר
|
||||
|
||||
3. **גרסאות עם מספור** (למשל `(1)`, `(2)`):
|
||||
- לזהות איזו הגרסה האחרונה
|
||||
- לשמור רק אותה
|
||||
|
||||
### תהליך זיהוי
|
||||
|
||||
```bash
|
||||
# השוואת שמות קבצים בין תיקיות
|
||||
comm -12 <(ls תיקייה1 | sort) <(ls תיקייה2 | sort)
|
||||
```
|
||||
|
||||
### כלל אצבע
|
||||
|
||||
אם קובץ מופיע ביותר מתיקייה אחת עם שם זהה - הוא כנראה משותף ויש להעבירו פעם אחת לתיקייה `משותף/`.
|
||||
|
||||
---
|
||||
|
||||
## תמלול מסמכי PDF ל-Markdown
|
||||
|
||||
### חובת תמלול
|
||||
|
||||
**כל קובץ PDF בתיק חייב להיות מתומלל לקובץ Markdown מקביל.** זה מאפשר:
|
||||
- חיפוש בתוכן המסמכים
|
||||
- העתקה והדבקה של ציטוטים
|
||||
- עריכה ועיבוד הטקסט
|
||||
- שמירה על הידע גם ללא גישה לכלי קריאת PDF
|
||||
|
||||
### תהליך התמלול
|
||||
|
||||
1. **זיהוי קבצי PDF** בתיקייה:
|
||||
```bash
|
||||
find . -name "*.pdf" -type f
|
||||
```
|
||||
|
||||
2. **תמלול כל PDF** באמצעות:
|
||||
```
|
||||
mcp__gemini-vision__analyze_document
|
||||
- document_path: [נתיב לקובץ]
|
||||
- prompt: "תמלל את המסמך במלואו בעברית. שמור על המבנה המקורי - כותרות, פסקאות, רשימות ממוספרות. אם יש טבלאות, שמור עליהן בפורמט markdown."
|
||||
```
|
||||
|
||||
3. **טיפול בתוצאה ארוכה** ⚠️ מסמכים משפטיים הם בדרך כלל ארוכים (20-60 עמודים). כש-`analyze_document` מחזיר תוצאה שחורגת ממגבלת הטוקנים:
|
||||
- התוצאה נשמרת אוטומטית לקובץ זמני (הנתיב מופיע בהודעת השגיאה)
|
||||
- **השתמש ב-Task agent** (subagent_type: general-purpose) כדי לקרוא את הקובץ הזמני בחלקים (offset/limit) ולשמור אותו כ-MD
|
||||
- אל תנסה לקרוא את הקובץ הזמני ישירות - הוא בדרך כלל חורג מ-25K טוקנים
|
||||
|
||||
4. **שמירת התמלול** כקובץ MD:
|
||||
- באותה תיקייה כמו ה-PDF
|
||||
- עם אותו שם קובץ
|
||||
- סיומת `.md` במקום `.pdf`
|
||||
|
||||
### פורמט התמלול
|
||||
|
||||
```markdown
|
||||
# [כותרת המסמך]
|
||||
|
||||
**תאריך:** [תאריך המסמך]
|
||||
|
||||
**לכבוד:** [נמען]
|
||||
|
||||
---
|
||||
|
||||
## 1. [כותרת סעיף ראשון]
|
||||
|
||||
1.1. [תוכן תת-סעיף]
|
||||
|
||||
1.2. [תוכן תת-סעיף]
|
||||
|
||||
---
|
||||
|
||||
## חתימות
|
||||
|
||||
| שם | מ.ז |
|
||||
|---|---|
|
||||
| [שם] | [מספר זהות] |
|
||||
```
|
||||
|
||||
### סוגי מסמכים לתמלול
|
||||
|
||||
| סוג מסמך | עדיפות | הערות |
|
||||
|----------|--------|-------|
|
||||
| כתבי ערר | גבוהה | מכילים את טענות העוררים |
|
||||
| כתבי תשובה | גבוהה | מכילים את עמדת המשיבים |
|
||||
| פרוטוקולי דיון | גבוהה | מתעדים את הנאמר בדיון |
|
||||
| החלטות קודמות | בינונית | לצורך הפניות ופסיקה |
|
||||
| חוות דעת | בינונית | מסמכים טכניים |
|
||||
| נספחים | נמוכה | לפי הצורך |
|
||||
537
skills/decision/SKILL.md
Normal file
537
skills/decision/SKILL.md
Normal file
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: legal-decision
|
||||
description: This skill should be used when writing legal decisions (החלטות) for betterment levy appeals (היטל השבחה) and licensing appeals (רישוי). It applies low temperature for accuracy and high token output for elaborate legal analysis with citations from case law.
|
||||
---
|
||||
|
||||
# סגנון כתיבת החלטות - דפנה תמיר
|
||||
|
||||
מדריך זה מלמד לכתוב החלטות של ועדת ערר לתכנון ובניה בסגנון של יו"ר הוועדה דפנה תמיר. המדריך עוסק אך ורק בסגנון הכתיבה, במבנה, בביטויים ובשיטה האנליטית. הפסיקה הרלוונטית תסופק בכל ערר בנפרד.
|
||||
|
||||
סוגי עררים: היטל השבחה (מספרי 8xxx), רישוי ובנייה (מספרי 1xxx), תכנון (מספרי 1xxx).
|
||||
|
||||
|
||||
## 1. זיהוי סוג הערר והטון
|
||||
|
||||
מספר הערר קובע את הטון. ערר רישוי (1xxx) נכתב בטון חם יחסית עם אלמנטים אנושיים בסיום. ערר היטל השבחה (8xxx) נכתב בטון קר, יבש ומקצועי, ללא רגשות.
|
||||
|
||||
סוג הערר משפיע על טון הכתיבה, על פתיחת הדיון (רחבה עם הקשר תכנוני ברישוי לעומת ישירה בהיטל השבחה), על סיום ההחלטה (פסקת סיום חמה ברישוי שנדחה לעומת סיום יבש בהיטל השבחה), ועל יחסי החלקים במסמך.
|
||||
|
||||
|
||||
## 2. מבנה המסמך
|
||||
|
||||
### 2.1 כותרת והרכב
|
||||
|
||||
המסמך נפתח בכותרת מובנית הכוללת: שם המוסד (מדינת ישראל, ועדת ערר לתכנון ובניה, מחוז ירושלים), מספר תיק, הרכב הוועדה (יו"ר וחברים), שמות הצדדים ובאי כוחם, והמילה "החלטה".
|
||||
|
||||
### 2.2 סדר הפרקים
|
||||
|
||||
הרקע העובדתי מתחיל ישירות בסעיף 1, ללא כותרת מפורשת. לאחריו באים: "תמצית טענות הצדדים" עם תת-פרקים לכל צד (טענות העוררים, עמדת הוועדה המקומית, עמדת מבקשי ההיתר), ואז "דיון והכרעה", ולבסוף "סיכום" או "סוף דבר".
|
||||
|
||||
כשהוועדה המקומית קיימה דיון מנומק, ייתכן פרק נפרד שמצטט את הפרוטוקול לפני הטענות. כשהדיון בפני ועדת הערר הוסיף מעבר לכתבי הטענות, ייתכן תת-פרק "הדיון בפני ועדת הערר" או "הבהרות".
|
||||
|
||||
כותרת הסיום: "סיכום" מועדף בעררי היטל השבחה ובדחיות. "סוף דבר" מועדף בעררי רישוי ובקבלות. לא "סיכום והכרעה".
|
||||
|
||||
### 2.3 כותרות משנה
|
||||
|
||||
כותרות משנה מופיעות רק כשיש הבחנה בין צדדים (טענות העוררים, הוועדה המקומית, מבקשי ההיתר) או בין נושאים מרכזיים שונים. בפרק הדיון וההכרעה הכלל הוא אסה רציפה אחת ללא כותרות משנה, והמעברים נעשים באמצעות ביטויי מעבר טקסטואליים. החריג: כשיש מספר נושאים נפרדים לחלוטין (למשל: "הבקשה להקלה בגובה המבנה" ואז "התייחסות לטענות נוספות שעלו בכתב הערר"), מותרת כותרת נושאית.
|
||||
|
||||
### 2.4 מספור סעיפים
|
||||
|
||||
מספרים עוקבים לאורך כל המסמך: 1, 2, 3 וכו'. אין איפוס מספור בין פרקים. המספור ממשיך ברצף מ-1 עד הסוף.
|
||||
|
||||
|
||||
## 3. סגנון ניסוח
|
||||
|
||||
### 3.1 אורך סעיפים
|
||||
|
||||
בחלק הרקע והטענות: סעיף טענה רגיל הוא 40-60 מילים (פתיחה, טענה, פירוט, השלכה - לא פחות מ-3 משפטים). סעיף תגובה הוא 50-80 מילים (דחיית הטענה, נימוק, ראיה). סעיף עם ציטוט הוא 80-150 מילים (הקדמה, ציטוט, הסבר). סעיפי גשר בלבד הם 15-30 מילים ("נבהיר כי...", "כך גם נציין..."). הממוצע הכולל: 45-55 מילים לסעיף.
|
||||
|
||||
בחלק הדיון וההכרעה: הסעיפים ארוכים יותר משמעותית. סעיף ניתוח רגיל הוא 40-80 מילים. סעיף עם ציטוט מקיף מפסיקה או מהחלטה מרכזת יכול להגיע ל-200-600 מילים - ואין לפצל ציטוט כזה למספר סעיפים. הממוצע בדיון: 80-120 מילים לסעיף כולל ציטוטים.
|
||||
|
||||
דוגמה לסעיף טענה תקין (כ-40 מילים): "העוררים טוענים כי נפל פגם מהותי בהליך ההמצאה. לטענתם, תכניות הבנייה והבקשה להיתר לא הומצאו לידיהם כנדרש. העוררים מדגישים כי אי-ההמצאה מנעה מהם את האפשרות להתנגד במועד, ולטענתם מדובר ב'מחטף' שנועד לעקוף את זכות ההתנגדות של בעלי הזכויות בנכס."
|
||||
|
||||
### 3.2 יחסי הזהב - חלוקת אחוזים
|
||||
|
||||
ערר רישוי שנדחה (1xxx): פתיחה 0.5%, רקע 15-25%, טענות 30-40%, דיון 37-50%, סיכום 2-9%. מאפיינים: פתיחת דיון עם הקשר תכנוני רחב (5-8 פסקאות), אלמנטים אנושיים בסיום.
|
||||
|
||||
ערר רישוי שמתקבל (1xxx): רקע כולל ציטוט מפרוטוקול הוועדה 30-40%, טענות כולל השלמת מסמכים 20-30%, דיון עם ניתוח נימוק-נימוק 35-45%, סוף דבר 3-5%. מאפיינים: פתיחת דיון ישירה, דיון ארוך עם חזרות מכוונות, סיום עניני.
|
||||
|
||||
ערר רישוי שמתקבל חלקית (1xxx): רקע כולל ציטוט מפרוטוקול 25-35%, טענות כולל השלמת טיעון 25-30%, דיון 40-47%, סיכום 2-3%. מאפיינים: פתיחת דיון עם מיפוי מתחים (3-6 סעיפים), דיון ארוך עם ציטוטים נרחבים מהוראות תכנית ונספחים, סיכום מינימלי-אופרטיבי. ראה: בית הכרם 1126/25.
|
||||
|
||||
ערר היטל השבחה (8xxx): פתיחה 5-7%, רקע 6-18%, טענות 13-25%, דיון 32-48%, סיכום 3-4%. מאפיינים: פתיחה ישירה, הרבה ציטוטי פסיקה, סיום יבש.
|
||||
|
||||
### 3.3 הגדרות בשיטת "להלן"
|
||||
|
||||
פורמט קבוע: (להלן: "שם ההגדרה"). ההגדרה מופיעה במירכאות כפולות, בסוגריים לאחר האזכור הראשון. ניתן להגדיר שני מונחים חלופיים עם קו נטוי: (להלן: "התכנית" או "תכנית הל/435"). דוגמאות: (להלן: "הבקשה להיתר"), (להלן: "הבניין" / "המקרקעין"), (להלן: "התקנות").
|
||||
|
||||
### 3.4 ציטוטים ארוכים
|
||||
|
||||
ציטוטים ארוכים מפרוטוקולים, תכניות או פסיקה מוצגים כבלוק נפרד עם כניסה. לעיתים עם הערה "(הדגשת הח"מ)" או "(הדגשת הח.מ.)". ציטוטים ארוכים הם כלי מרכזי - דפנה מעדיפה לצטט בלוק ארוך אחד (200-500 מילים) מתוך החלטה מרכזת שכבר ריכזה את הפסיקה, במקום לצטט כל פסק דין בנפרד.
|
||||
|
||||
### 3.5 תמונות ונספחים
|
||||
|
||||
תמונות ותשריטים משולבים בטקסט עם הפניה מילולית: "להלן מתוך נספח האדריכלות והבקשה להיתר שם ניתן לראות את הדברים:", "להלן תמונת המגרשים הרלוונטיים מתוך מערכת ה-GIS של הוועדה המקומית", "להלן מתוך נספח הבינוי המצורף להוראות התכנית". העיצוב הטכני נעשה בשלב הוורד.
|
||||
|
||||
### 3.6 קיצורים משפטיים
|
||||
|
||||
עו"ד (עורך דין), ב"כ (בא כוח), עוה"ד (עורך הדין), יח"ד (יחידת דיור), מ"ר (מטר רבוע), תב"ע (תכנית בניין עיר), רמ"י (רשות מקרקעי ישראל), ר"ר / בר"ר (רשות רישוי), סה"כ (סך הכל).
|
||||
|
||||
### 3.7 פורמט תאריכים ומספרים
|
||||
|
||||
תאריכים: DD.MM.YY (לדוגמה: 12.06.25 או 17.4.2024). מספר תכנית: 102-1170893 או הל/435. מספר בקשה להיתר: 20250337. גוש וחלקה: גוש 30322 חלקה 77 מגרש 13.
|
||||
|
||||
### 3.8 עקרונות כלליים
|
||||
|
||||
הקורא צריך להבין את ההקשר המלא מקריאת ההחלטה בלבד. תן הקשר מלא ואל תניח שהקורא מכיר את התיק. ציטוט עדיף על תמצות כשיש מסמך מקור. פרט מספרים ונתונים מדויקים ("10 מתוך 14 דירות (71.4%)" ולא רק "רוב"). הסבר מונחים ("במסלול הירוק, קרי היתרים תואמי תכנית").
|
||||
|
||||
|
||||
## 4. הצגת טענות הצדדים
|
||||
|
||||
### 4.1 טענות העוררים
|
||||
|
||||
מבנה סעיף: מספר, תיאור הטענה, פירוט, השלכות לטענת העוררים. ביטויים: "העוררים טוענים כי...", "לטענתם...", "העוררים מדגישים כי...", "עוד ציינו כי...", "לטענתם מדובר ב...", "העורר מוסיף וטוען כי...", "לחיזוק טענתו, מצביע העורר על...", "לחיזוק טענותיו...". טענות מהדיון: "בדיון הוסיפו וטענו העוררים כי...", "בדיון הוסיפה וטענה העוררת גב' [שם] כי...". הבהרות: "במסגרת הודעת העורר כי הצדדים לא הגיעו לפתרון מוסכם ביניהם צירף העורר הבהרה וטען כי...".
|
||||
|
||||
### 4.2 עמדת הוועדה המקומית
|
||||
|
||||
פתיחה: "במסגרת כתב התשובה ובדיון בפנינו הוועדה המקומית הציגה את עמדתה באופן מפורט." ביטויים: "הטענה המרכזית הינה כי...", "הוועדה המקומית הציגה נתונים מפורטים לעניין...", "הוועדה המקומית הבהירה כי...", "הוועדה הוסיפה כי...", "הוועדה המקומית טוענת כי...", "הוועדה המקומית מוסיפה כי...", "עוד טוענת הוועדה המקומית כי...", "לבסוף טוענת הוועדה המקומית כי...".
|
||||
|
||||
### 4.3 טענות מבקשי ההיתר
|
||||
|
||||
ביטויים: "מבקשי ההיתר דוחים מכל וכל את הטענה...", "לטענתם...", "מבקשי ההיתר מציינים כי...", "מבקשי ההיתר מבהירים כי...", "עוד מציינים מבקשי ההיתר כי...", "מכל מקום, מבקשי ההיתר...", "לגופו של עניין...", "לאור האמור, מבקשי ההיתר עותרים ל...".
|
||||
|
||||
### 4.4 כללים
|
||||
|
||||
הפרדה ברורה בין הצדדים - כל צד בפרק נפרד. סדר קבוע: עוררים, וועדה מקומית, מבקשי היתר. אין שילוב של טענות צדדים שונים באותו סעיף.
|
||||
|
||||
|
||||
## 5. ביטויים ומעברים
|
||||
|
||||
### 5.1 ביטויים כלליים
|
||||
|
||||
"לפנינו" - פתיחת המסמך ("לפנינו שני עררים..."). "לענייננו" - מיקוד בנקודה רלוונטית. "נבהיר כי" - הבהרה חשובה. "כך גם נציין כי" - הוספת נקודה משלימה. "מדובר ב..." - הסבר עובדתי. "כפי ש..." - הפניה למידע קודם. "ואשר עניינם/עניינה ב..." - הגדרת נושא. "ככל ש..." - תנאי. "הרי ש..." - מסקנה. "מכל מקום" - מעבר לטענה חלופית. "לגופו של עניין" - מעבר לדיון המהותי. "אדרבא" - טענה נגדית חזקה. "קרי" - הסבר/פירוש. "כאמור" - הפניה לדבר שנאמר קודם. "בענייננו" - מעבר מכלל לפרט.
|
||||
|
||||
### 5.2 ביטויי מעבר בפרק הדיון
|
||||
|
||||
"ועל מנת לא לצאת בחסר" - פתיחת obiter dicta / נימוקים נוספים. דוגמה: "אך יחד עם זאת ועל מנת לא לצאת בחסר מצאנו להוסיף..."
|
||||
|
||||
"נציין כי טענות אלו נטענו בלשון רפה" - הכרה שטענות חלשות אבל נדונות. דוגמה: "...אך יחד עם זאת נדרשנו אליהם מכח הלכת שפר."
|
||||
|
||||
"עינינו הרואות" - סיכום ביניים לאחר ציטוט ארוך.
|
||||
|
||||
"נוסיף." - מעבר קצר (משפט אחד) לפני ציטוט נוסף.
|
||||
|
||||
"אם כן" / "אם כך" - הסקת מסקנה מציטוט או ניתוח. דוגמה: "אם כך, לעת הזו, הגישה הנוהגת היא ש..."
|
||||
|
||||
"למעלה מן הצורך" - דיון שאינו הכרחי להכרעה אבל נכתב מטעמים אסטרטגיים.
|
||||
|
||||
"ברי כי" - מסקנה מובנת מאליה.
|
||||
|
||||
"נפנה ל..." - פתיחת ניתוח של סעיף חוק/פסיקה.
|
||||
|
||||
"למיטב הבנתנו" - עמדה זהירה בסוגיה שטרם הוכרעה.
|
||||
|
||||
"נשלים ונציין" - תוספת אחרונה לפני הסיכום.
|
||||
|
||||
"מכל האמור לעיל" - מעבר לסיכום.
|
||||
|
||||
"נשוב על כך כי" - חזרה מכוונת על עקרון חשוב שכבר נאמר. דוגמה: "נשוב על כך כי ההחלטה לאישור הבקשה אין בה בכדי להוות כל הכרעה קניינית."
|
||||
|
||||
"דא עקא" - הצגת הבעיה המרכזית או מה שסותר את הטענה.
|
||||
|
||||
"ובמילים אחרות" - חידוד/פרפרזה של נקודה שנאמרה.
|
||||
|
||||
"הגענו לכלל מסקנה כי" - מסקנה מרכזית (בפתיחת דיון).
|
||||
|
||||
"לא נוכל לקבל" - דחיית עמדה/טענה.
|
||||
|
||||
"מקובלת עלינו" - קבלת עמדה.
|
||||
|
||||
"התרשמנו כי" - מסקנה מהדיון/ממסמכים.
|
||||
|
||||
"נחדד כי" - חידוד נקודה קודמת.
|
||||
|
||||
"סיכומם של דברים" - פתיחת סיכום מהותי לפני "סוף דבר".
|
||||
|
||||
"המסקנה מכל האמור היא כי" - מסקנת ביניים מקיפה.
|
||||
|
||||
"לעמדתנו" - עמדת הוועדה.
|
||||
|
||||
"בנסיבות אלה" - מעבר מעובדות למסקנה.
|
||||
|
||||
"נזכיר כי" - תזכורת לעקרון ידוע.
|
||||
|
||||
"מצאנו כי" - קביעה עובדתית.
|
||||
|
||||
"שוכנענו כי" - קביעה לאחר בחינה.
|
||||
|
||||
"על כן ולו רק מסיבה זו" - ניטרול טענה חלשה לפני ניתוח מעמיק.
|
||||
|
||||
"יחד עם זאת, מצאנו לנכון לדון בשאלה העקרונית" - מעבר לדיון עקרוני למרות דחייה.
|
||||
|
||||
"נסכם כי" - מעבר לסיכום ביניים.
|
||||
|
||||
"משכך" - הסקת מסקנה מהאמור.
|
||||
|
||||
"הדברים מתחדדים שעה ש..." - חידוד נוסף לאור נסיבות נוספות.
|
||||
|
||||
"נחזור על כך כי" - חזרה מודגשת על קביעה חשובה.
|
||||
|
||||
"זאת ועוד" - הוספת נימוק נוסף.
|
||||
|
||||
"יתרה מכך" - חיזוק הנימוק הקודם.
|
||||
|
||||
"לאור כל האמור לעיל" - פתיחת סיכום סופי.
|
||||
|
||||
"נפתח בכך כי" - פתיחה של הדיון (לא של המסמך).
|
||||
|
||||
"נפנה בעניין זה להחלטת..." - הפניה לתקדים.
|
||||
|
||||
"הדברים משליכים על שיקול הדעת ב..." - קישור בין ממצא למסקנה.
|
||||
|
||||
"רוצה לומר כי" - הסבר/פרפרזה אלטרנטיבית.
|
||||
|
||||
"נוצר מצב בו" - הצגת בעיה/מצב עובדתי.
|
||||
|
||||
"לכך נוסיף כי" - הוספת נדבך נוסף לטיעון.
|
||||
|
||||
"יש אולי להצר על כך ש..." - הערה ביקורתית עדינה (כלפי מוסד תכנון).
|
||||
|
||||
"עם ההבנה לטענה זו של העוררים, אין בידנו לקבלה" - acknowledge-reject מרוכך.
|
||||
|
||||
"ברי כי משאב הקרקע יקר לבעליו ולציבור" - הצדקת שימוש יעיל בקרקע.
|
||||
|
||||
|
||||
## 6. השיטה האנליטית - "איך לחשוב" לפני "איך לכתוב"
|
||||
|
||||
### 6.1 שומר הסף - שאלת הסף
|
||||
|
||||
לפני שנוגעים בטענה לגופה, השאלה הראשונה היא: "יש לעוררים בכלל זכות ערר?" זו לא רק שאלה פרוצדורלית - זו מסגרת הניתוח כולה. **סייג חשוב:** שאלת הסף היא כלי אסטרטגי, לא חובה. בתיקים עם שאלות מהותיות חזקות (חניה, שימור, קווי בניין), דפנה עשויה לדלג על שאלת הסף ולדון ישירות בגוף העניין — במיוחד בקבלה חלקית. ראה: בית הכרם 1126/25 — דילגה על ס' 152 לחלוטין.
|
||||
|
||||
### 6.2 קילוף מסכות - סיווג הטענות
|
||||
|
||||
לוקחים כל טענה ובוחנים מה מתחת למסכה. האם טענה של "סטייה מתכנית" היא באמת סטייה, או השגה על התכנית עצמה? האם טענה קניינית לבשה כסות תכנונית? הטכניקה: לכתוב במפורש "טענות שהוצגו בכסות של טענות תכנוניות" - לחשוף את האסטרטגיה, לא רק לדחות אותה.
|
||||
|
||||
### 6.3 עיגולים קונצנטריים - בניית שכבות
|
||||
|
||||
לא לענות על כל טענה בנפרד. לבנות פירמידה של שכבות הגנה: שכבה חיצונית (אין זכות ערר כלל), "אך יחד עם זאת..." שכבה שנייה (גם אם הייתה - אין סטייה מתכנית), "למעלה מן הצורך..." שכבה שלישית (גם על המריטים - ההחלטה נכונה), "עוד נשלים..." שכבה רביעית (הטענות הקנייניות - מקומן בערכאה אחרת). ההחלטה נהיית חסינה מערעור - גם אם בית המשפט לא יקבל שכבה אחת, השכבות האחרות עומדות בפני עצמן. **הערה:** מבנה השכבות מתאים בעיקר לערר שנדחה (כמו הכט 1180-1181). בקבלה חלקית, דפנה עשויה להשתמש במבנה חלופי: הקשר → מיפוי נקודות מתח → ניתוח נושא-נושא → הוראות מעשיות. ראה: בית הכרם 1126/25.
|
||||
|
||||
### 6.4 הכרה-דחייה-ניתוב
|
||||
|
||||
לעולם לא לומר שהטענה לא לגיטימית. במקום זאת: להכיר ("אכן, לעוררים עומדת זכות להגיש התנגדות קניינית"), לדחות ("אולם הדין לא נתן בידי אותו בעל זכות גם זכות ערר"), לנתב ("מקומן של הטענות להתברר בפני המפקח על הבתים המשותפים").
|
||||
|
||||
### 6.5 מיפוי מצב הדין
|
||||
|
||||
כשיש שאלה משפטית שטרם הוכרעה, לא לבחור צד ולצטט. למפות את כל המצב: פסיקה A (דעת רוב - כיוון X), עתירה/ערעור, עמדת המדינה (כיוון Y), פסיקה B (חזרה מ-A), ערעור תלוי ועומד (טרם הוכרע), מסקנה: "לעת הזו, הגישה הנוהגת היא ש..."
|
||||
|
||||
### 6.6 נדיבות אסטרטגית - למעלה מן הצורך
|
||||
|
||||
השימוש ב"למעלה מן הצורך" הוא אסטרטגי: מראה שגם על המריטים התוצאה זהה, מונע טענה של "לא שמעו אותנו", יוצר תקדים גם בשאלות שלא היו חייבים להכריע, בונה קו פסיקתי עקבי.
|
||||
|
||||
### 6.7 ניטרול טענה חלשה לפני ניתוח מעמיק
|
||||
|
||||
כשיש טענה שאפשר לדחות מסיבה טכנית פשוטה, אבל יש גם שאלה עקרונית מעניינת מאחוריה - דפנה קודם מנטרלת את הטענה ("על כן ולו רק מסיבה זו נכון היה לדחות את הערעור ולסיים את הדיון"), ואז עוברת לדיון עקרוני ("יחד עם זאת, מצאנו לנכון לדון בשאלה העקרונית האם..."). זה מאפשר ליצור תקדים עקרוני מבלי להסתמך על הטענה החלשה.
|
||||
|
||||
### 6.8 טכניקת החידוד ("שארפנינג")
|
||||
|
||||
חזרה מכוונת על נקודות מפתח, כאשר כל חזרה מוסיפה זווית חדשה. דפנה משתמשת ב"נחדד כי...", "נשוב על כך כי...", "הדברים מתחדדים שעה ש...", "נחזור על כך כי..." - כל אחד מהביטויים האלה לא חוזר על מה שנאמר אלא מוסיף שכבה נוספת של ניתוח.
|
||||
|
||||
### 6.9 מנגנון פרשנות תכנית
|
||||
|
||||
כשצריך לפרש תכנית בניין עיר, דפנה עוברת ארבעה שלבים: ראשית, בחינת לשון התכנית (ציטוט מדויק מההוראות, בחינה האם ההוראה מחייבת או מנחה, חיפוש ניסוחים מחייבים כמו "בכל מקרה", "לא יעלה על"). שנית, בחינת תכלית התכנית (מה המטרות שהוגדרו, האם מדובר בתכנית נקודתית או כוללנית, מי היו היוזמים). שלישית, בחינת ההיסטוריה התכנונית (מה קבעו תכניות קודמות, האם התכנית החדשה החמירה או הקלה, מה המגמה). רביעית, בחינה ראייתית (היתרים שניתנו באזור, תצלומים, תשריטים, הדמיות).
|
||||
|
||||
### 6.10 ניתוח "בית בודד" בתמ"א 38
|
||||
|
||||
כשעוסקים בחיזוק/הריסה של בית בודד (להבדיל מבניין מגורים רב-קומתי), אינטרס החיזוק מפני רעידות אדמה מוחלש. דפנה מציינת זאת במפורש: "עסקינן בחיזוק בית בודד ועל כן... לא קיים באופן מלא אינטרס חיזוק כזה המצדיק את אישור מלוא הזכויות." המשמעות: שיקול דעת זהיר יותר באישור מלוא הזכויות מכוח תכנית 10038, במיוחד בנושאי קווי בניין וחניה. "הדברים משליכים על שיקול הדעת באישור הבקשה להיתר." ראה: בית הכרם ס' 41.
|
||||
|
||||
### 6.11 תכנית אב כ"מגן" מפני תכנון אד-הוק
|
||||
|
||||
כשקיימת תכנית אב/מדיניות לאזור, דפנה משתמשת בה כדי לאמת שהיתר בודד לא יוצר חריג: "קיימת תכנית אב אשר מקלה על בחינת הבקשה... החשש לאישור היתר מכח תכנית 10038 על מגרש בודד ללא ראיה כללית אינו קיים למעשה." דפוס: בדוק אם יש מדיניות → ציטוט הסעיפים הרלוונטיים → מסקנה שההיתר "משתלב עם ראיה כללית קיימת" ולא "מכתיב תכנון עתידי לסביבה". ראה: בית הכרם ס' 42.
|
||||
|
||||
|
||||
## 7. כתיבת פרק הדיון וההכרעה
|
||||
|
||||
### 7.1 עקרונות כתיבה
|
||||
|
||||
אסה רציפה אחת - ללא כותרות משנה (חריג: נושאים נפרדים לחלוטין). מסקנה בפתיחה - הפרק נפתח עם המסקנה ואז מפרט את הנימוקים. ציטוטים ארוכים ומקיפים - העדפה לבלוק ציטוט אחד ארוך על פני פיצול. הפניה להחלטות מרכזות - במקום לצטט כל פסק דין בנפרד, הפניה להחלטה שכבר ריכזה את הפסיקה. שכבות ניתוח - מסקנה, נימוק ראשי, "אך יחד עם זאת", נימוק משני, "למעלה מן הצורך", obiter.
|
||||
|
||||
### 7.2 פתיחת הדיון - לפי סוג ערר ותוצאה
|
||||
|
||||
ערר רישוי שנדחה (פתיחה רחבה, 5-8 סעיפים): הדיון נפתח עם הקשר תכנוני רחב שמכין את הקורא. הקשר פרוצדוראלי (1-2 פסקאות) על ניסיון גישור שלא צלח. מהות דיני התכנון כעקרון (2-3 פסקאות) על יצירת פיתוח ואיכות חיים. איזונים בתכנון (1-2 פסקאות) על התחשבות בכל המעורבים. ערך ההתחשבות באחר (1-2 פסקאות). ייחוד המקרה (1 פסקה) - "בענייננו, אנו נדרשים לערוך את האיזונים הללו ולבחון האם...". רק אחרי 5-8 פסקאות אלה - צלול לעניין הספציפי עם מסקנה.
|
||||
|
||||
ערר רישוי שמתקבל (פתיחה ישירה, 1-2 סעיפים): "לאחר שמיעת טענות הצדדים ועיון במסמכים שהוגשו, הגענו לכלל מסקנה כי דין הערר להתקבל בכפוף למספר תיקונים בבקשה להיתר כפי שיורחב להלן." ואז הערה פרוצדורלית אם צריך.
|
||||
|
||||
ערר היטל השבחה (פתיחה ישירה עם מסקנה): "לאחר שבחנו את טענות הצדדים ונערך דיון בפנינו בו נשמעו בהרחבה, החלטנו בשלב ראשון כי [המסקנה]. אך יחד עם זאת ועל מנת לא לצאת בחסר ומאחר ונשמעו הצדדים בפנינו מצאנו להוסיף מספר הערות והכל כפי שיפורט להלן;"
|
||||
|
||||
ערר על פרשנות תכנית / סטיה ניכרת (פתיחה עם שאלה מרכזית): "כאמור השאלה המרכזית בערר הינה בחינת הבקשה ל[נושא הערר]." ואז ניטרול טענות חלשות, ומעבר לדיון עקרוני.
|
||||
|
||||
ערר רישוי שמתקבל חלקית (פתיחה במיפוי מתחים, 3-6 סעיפים): 1-2 פסקאות כלליות על ערך התכנון ואיזון אינטרסים. "בערר דנן עולות שאלות כיצד והאם..." → רשימה של 4-6 נקודות מתח ספציפיות בתיק (בבולטים), כל נקודה מתארת ציר מתח בין אינטרסים מנוגדים. "כל הנקודות לעיל עומדות לפנינו ולשם כך קראנו, שמענו את הצדדים..." → מעבר לניתוח. ראה: בית הכרם ס' 37-40.
|
||||
|
||||
### 7.3 מסלול דיון לפי סוג ערר
|
||||
|
||||
**ערר רישוי שנדחה - מסלול שכבות הגנה:**
|
||||
פתיחה רחבה עם הקשר תכנוני (5-8 סעיפים). ניתוח זכות הערר (2-4 סעיפים). טענות קנייניות - אין זכות ערר (1-2 סעיפים ארוכים עם ציטוט מקיף מהחלטה מרכזת). דחיית טענות סטייה מתכנית (3-5 סעיפים). "למעלה מן הצורך" - היתכנות קניינית (אופציונלי). התייחסות לטענות ספציפיות (2-3 סעיפים קצרים).
|
||||
|
||||
**ערר רישוי שמתקבל - מסלול נימוק-נימוק:**
|
||||
פתיחה ישירה עם מסקנה. הדיון מאורגן לפי נימוקי הדחייה של הוועדה המקומית. לכל נימוק: הצגת הנימוק ("באשר ל[נימוק] - הוועדה המקומית קבעה כי..."), ניתוח ("מעיון ב... / לא נוכל לקבל... / מקובלת עלינו..."), מסקנת ביניים ("משכך..." / "על כן..."). כשיש היבט קנייני - "נשוב על כך כי..." מופיע לפחות פעמיים (בדיון ובסיכום).
|
||||
|
||||
**ערר על פרשנות תכנית / סטיה ניכרת:**
|
||||
הצגת שאלה מרכזית (1 פסקה). ניטרול טענות חלשות (1-2 פסקאות) - "על כן ולו רק מסיבה זו נכון היה לדחות". מעבר לדיון עקרוני (1 פסקה) - "יחד עם זאת, מצאנו לנכון לדון בשאלה העקרונית". ניתוח מקיף (5-15 פסקאות) באמצעות מנגנון פרשנות התכנית בארבעת שלביו: לשון, תכלית, היסטוריה תכנונית, בחינה ראייתית. ציטוטי פסיקה משולבים (2-3 פסקאות). מסקנה (1-2 פסקאות) עם "נסכם כי" או "לאור כל האמור לעיל".
|
||||
|
||||
**ערר היטל השבחה:**
|
||||
פתיחה ישירה עם מסקנה. ניתוח ישיר - ציטוטי פסיקה מרובים. סיום יבש.
|
||||
|
||||
**ערר רישוי שמתקבל חלקית — מסלול מיפוי מתחים + ניתוח נושאי:**
|
||||
פתיחה במיפוי מתחים (3-6 סעיפים): הקשר כללי קצר (1-2 פסקאות), רשימת נקודות מתח ספציפיות בתיק (4-6 בולטים), מעבר לניתוח. אין שימוש בשכבות/עיגולים קונצנטריים — ניתוח לפי נושאים: כל נושא מקבל טיפול מלא (הצגה → ציטוט הוראות תכנית → פסיקה → מסקנה). נושא חניה/תשתיות מקבל טיפול מעמיק במיוחד עם ציטוטים ישירים מהוראות תכנית ונספחים. טענות ספציפיות (מטרדים, עצים, בור מים) — 1-2 סעיפים תמציתיים לכל אחת. סיכום מינימלי — רק הוראות אופרטיביות (2-3 סעיפים). ראה: בית הכרם 1126/25.
|
||||
|
||||
### 7.4 טכניקת "ציטוט דרך החלטה מרכזת"
|
||||
|
||||
במקום לצטט כל פסק דין בנפרד, דפנה מפנה להחלטה שכבר ריכזה את הפסיקה: "בכל הנוגע ל[נושא], נפנה לניתוח המקיף שערכה ועדת הערר במסגרת ערר [שם] (פורסם בנבו) משם עולה כי..." ואז ציטוט בלוק ארוך (200-500 מילים) מתוך ההחלטה המרכזת שכוללת הפניות לפסיקה רלוונטית. הסיום: "אם כך, לעת הזו, הגישה הנוהגת היא ש..."
|
||||
|
||||
|
||||
## 8. כתיבת סיכום / סוף דבר
|
||||
|
||||
### 8.1 ערר שנדחה
|
||||
|
||||
הכותרת: "סיכום". פתיחה: "לאור כל האמור לעיל, הערר נדחה." מבנה: תתי-סעיפים עם אותיות עבריות (א. ב. ג. ד. ה. ו.), כל תת-סעיף = נימוק מסכם.
|
||||
|
||||
בערר רישוי שנדחה בלבד - לפני "הערר נדחה", מופיעה פסקת סיום חמה ואנושית. לדוגמה: "טרם סיום נבקש לציין כי כתיבת החלטה זו לא הייתה קלה והדיון נשמע בפנינו בזמנים לא קלים. אנו מצרים כפי שציינו כי הצדדים לא הגיעו לידי הבנות והלוואי ולא היינו נדרשים לכתיבת ההחלטה. אנו תקווה כי חרף אי ההסכמות ישכילו הצדדים לייצר יחסי שכנות טובה כל אחד בדרכו ובעולמו תוך התחשבות והבנה הדדית לצרכי האחד כלפי שכנו." בערר היטל השבחה שנדחה - סיום יבש בלבד.
|
||||
|
||||
### 8.2 ערר שמתקבל
|
||||
|
||||
הכותרת: "סוף דבר". פתיחה: "לאור כל האמור לעיל, הערר מתקבל בכפוף ל..." או "אשר על כן, לאחר שמיעת הצדדים...". מבנה: סיכום רציף בפרוזה (3-5 פסקאות), כל פסקה מסכמת נימוק אחד, הוראה אופרטיבית. אין פסקה חמה. הסיום עניני: "בנסיבות העניין, אין צו להוצאות."
|
||||
|
||||
### 8.3 ערר שמוחזר לדיון
|
||||
|
||||
הכותרת: "סיכום" או "סוף דבר". מבנה: הוראות ממוספרות לוועדה המקומית ("תבחן", "תערוך", "תדון").
|
||||
|
||||
### 8.4 ערר שמתקבל חלקית
|
||||
|
||||
הכותרת: "סיכום". פתיחה: "לאור כל האמור לעיל, הערר מתקבל באופן חלקי." מבנה: 2-3 סעיפים אופרטיביים בלבד — מה מתקבל (בבולד), מה נדחה, תנאים נוספים. ללא חזרה על נימוקים (ההנמקה כבר בדיון). ייתכן ללא התייחסות להוצאות. ללא פסקת סיום חמה. ראה: בית הכרם ס' 84-86.
|
||||
|
||||
### 8.5 חתימה
|
||||
|
||||
"ניתנה פה אחד, היום [תאריך עברי], [תאריך לועזי]." חתימות יו"ר ומזכירת ועדת הערר.
|
||||
|
||||
### 8.6 הוצאות
|
||||
|
||||
"בנסיבות העניין, אין צו להוצאות." (או "יש צו להוצאות בסך...") בקבלה חלקית — ייתכן שההוצאות לא יוזכרו כלל.
|
||||
|
||||
|
||||
## 9. דפוסים ייחודיים
|
||||
|
||||
### 9.1 חזרה מכוונת - "נשוב על כך כי"
|
||||
|
||||
בהחלטות קבלה שיש בהן היבט קנייני, דפנה חוזרת מספר פעמים על עקרון מרכזי: "נשוב על כך כי ההחלטה לאישור הבקשה אין בה בכדי להוות כל הכרעה קניינית ואין בה בכדי לגבור על כל הוראה אחרת על פי דין." כלל: בכל ערר שמתקבל ויש בו היבט קנייני, לשלב "נשוב על כך כי..." לפחות פעמיים - פעם בדיון ופעם בסיכום.
|
||||
|
||||
### 9.2 השלמת מסמכים
|
||||
|
||||
בתיקים מורכבים, לאחר הדיון ניתנת החלטה המאפשרת השלמת מסמכים. זה מתועד: "בסופו של דיון התקבלה החלטה המאפשרת השלמת מסמכים ואכן הגישו העוררים השלמת מסמכים הכוללת: [פירוט]. הוועדה המקומית השיבה וטענה כי [תשובה]." השלמת המסמכים מוצגת בסוף פרק הטענות.
|
||||
|
||||
### 9.3 הפניה לפרקדים של ניסיון גישור
|
||||
|
||||
"במסגרת הדיון בוועדת הערר הוסכם כי הצדדים ינסו לבחון דרכים להגיע לפתרון מוסכם ולבדוק דרך אפשרית לרבות קידום תוכנית נקודתית. במסגרת הודעת העורר כי הצדדים לא הגיעו לפתרון מוסכם ביניהם..."
|
||||
|
||||
### 9.4 עמידה על חוסר ראייתי
|
||||
|
||||
כשהעורר מעלה טענה בלי תימוכין: "מעבר לציון [מה שהוצג], לא הציג העורר כל תיעוד או אסמכתא להוכחת טענתו זו. בנסיבות אלה, לא עלה בידי העורר להרים את הנטל הראייתי הנדרש להוכחת טענתו בדבר [הטענה] כביכול."
|
||||
|
||||
### 9.5 למרות זאת - בדיקה עצמאית
|
||||
|
||||
גם כשהטענה לא הוכחה, דפנה בודקת בעצמה: "למרות זאת, ולמעלה מן הצורך, בחנה ועדת הערר את [מה שנבדק] והעלתה את הממצאים הבאים:..."
|
||||
|
||||
### 9.6 ציטוט הוראות תכנית
|
||||
|
||||
כשמצטטים מהוראות תכנית, דפנה מדגישה (בולד) את המילים המכריעות ומוסיפה ניתוח: "הניסוח הברור והחד-משמעי של ההוראה, בצירוף המילים 'בכל מקרה', מעיד על הכוונה ליצור מגבלה קשיחה."
|
||||
|
||||
### 9.7 אבחנה בין מחייב למנחה
|
||||
|
||||
"הנספח מבטא את נפח הבינוי המוצע והוא מנחה בלבד למעט לעניין [מה שמחייב]." הדגש המיוחד על מעמדן המחייב של הוראות מסוימות, להבדיל מיתר הוראות מנחות.
|
||||
|
||||
|
||||
## 10. רשימת בדיקה
|
||||
|
||||
### רקע עובדתי
|
||||
סעיף פתיחה "לפנינו..." או "עניינו בערר על החלטת...". תיאור הנכס והבקשה. הגדרות עם "להלן". ציטוט מהתכנית/פרוטוקול. תמונות רלוונטיות (מיקום מסומן). פירוט הליכים.
|
||||
|
||||
### טענות הצדדים
|
||||
פרק "תמצית טענות הצדדים". טענות העוררים. עמדת המשיבים (הוועדה המקומית + מבקשי ההיתר). מספור רציף לאורך כל הסעיפים.
|
||||
|
||||
### דיון והכרעה
|
||||
בדרך כלל אין כותרות משנה - הכל באסה רציפה (חריג: נושאים נפרדים). מסקנה בפתיחה. ציטוטים מקיפים מפסיקה בבלוקים ארוכים. ניטרול טענות חלשות לפני ניתוח מעמיק. מעבר לדיון עקרוני עם "יחד עם זאת, מצאנו לנכון...". שימוש בטכניקת החידוד ("נחדד כי", "נשוב על כך כי"). אם פרשנות תכנית - ארבעת השלבים (לשון, תכלית, היסטוריה, ראיות). אם קבלה עם היבט קנייני - "נשוב על כך כי..." לפחות פעמיים.
|
||||
|
||||
### סיכום / סוף דבר
|
||||
"סיכום" (דחייה/היטל השבחה) או "סוף דבר" (קבלה/רישוי). מבנה סיכום לפי תוצאה (תתי-סעיפים בדחייה, פרוזה בקבלה, הוראות בהחזרה). ערר רישוי שנדחה: פסקת סיום חמה ואנושית. הוצאות. חתימה עם תאריך עברי ולועזי.
|
||||
|
||||
### סגנון כללי
|
||||
שימוש ב"להלן" להגדרות. ביטויים ייחודיים לכל צד. ציטוטים ארוכים כבלוקים נפרדים. קיצורים משפטיים עקביים. אורך סעיפים מותאם (40-60 בטענות, 80-120+ בדיון). הקורא מבין את התיק רק מקריאת ההחלטה.
|
||||
|
||||
|
||||
## 11. מבנה ההחלטה — 12 בלוקים בסדר קבוע
|
||||
|
||||
### 11.1 עקרון מנחה: "מבחן השופט"
|
||||
|
||||
כל החלטה נכתבת כאילו שופט בית משפט לעניינים מנהליים קורא אותה לראשונה במסגרת עתירה מנהלית. השופט לא מכיר את התיק, לא ביקר בשטח, ולא שמע את הצדדים. כל מה שהוא יודע — הוא מה שכתוב בהחלטה. לכן:
|
||||
- הרקע מפורט ומלא (לא "כידוע" או "כמפורט בתיק")
|
||||
- ציטוטים ארוכים ולא מקוצרים (השופט צריך את המקור)
|
||||
- תמונות וחתכים משולבים (השופט לא היה בסיור)
|
||||
- כל טענה מקבלת מענה מפורש (שלא ייאמר "הוועדה לא התייחסה")
|
||||
- ההליכים מתועדים (שיהיה ברור שניתן יום בבית דין מלא)
|
||||
|
||||
### 11.2 סדר הבלוקים
|
||||
|
||||
להלן 12 הבלוקים בסדר קבוע. הסדר מחייב ואין לסטות ממנו. **להגדרות מלאות** (content model, constraints, משקלות, פרמטרי עיבוד) **ראה `references/block-schema.md`.**
|
||||
|
||||
| בלוק | שם | תפקיד | משקל |
|
||||
|------|-----|--------|------|
|
||||
| א | כותרת מוסדית | מזהה מוסד ותיק | 1% |
|
||||
| ב | הרכב הוועדה | מזהה הרכב מחליט | 1% |
|
||||
| ג | צדדים | מזהה עוררים ומשיבים | 1% |
|
||||
| ד | "החלטה" | סימון פורמלי | 0% |
|
||||
| ה | פתיחה | "לפנינו..." — מסגרת + הגדרות "להלן" | 1% |
|
||||
| ו | רקע ("פתח דבר") | עובדות ניטרליות + תמונות + ציטוט מפרוטוקול | 15-35% |
|
||||
| ז | טענות הצדדים | פרפרזה נאמנה — **רק כתבי טענות מקוריים** | 20-40% |
|
||||
| ח | הליכים בפני ועדת הערר | דיון, סיור, השלמות טיעון (עם תוכן), החלטות ביניים | 3-15% |
|
||||
| ט | תכניות חלות **(אופציונלי)** | ציטוט הוראות תכנית + ניתוח ראשוני | 0-12% |
|
||||
| י | **דיון והכרעה** | **ניתוח משפטי — CREAC — ליבת ההחלטה** | **32-50%** |
|
||||
| יא | סיכום / סוף דבר | תוצאה אופרטיבית בלבד | 2-9% |
|
||||
| יב | חתימות | "ניתנה פה אחד" + חתימות | 1% |
|
||||
|
||||
**שלושה כללים מרכזיים:**
|
||||
- ⚠️ **"רקע ניטרלי" (בלוק ו):** אם משפט מכיל ציטוט ישיר מצד, או מילות שיפוט ("חריג", "חטא") — הוא שייך לטענות (ז) או לדיון (י), לא לרקע. החלטות קודמות = עובדה יבשה בלבד.
|
||||
- ⚠️ **"ללא כפילות" (בלוק י):** הפנה לבלוקים קודמים ("כאמור בסעיף X"), אל תחזור עליהם. חריג: "נשוב על כך כי..." (חזרה מכוונת עם שכבה חדשה).
|
||||
- ⚠️ **טענות מקוריות בלבד (בלוק ז):** מכתבי ערר/תשובה. השלמות טיעון → בלוק ח עם תוכן מפורט.
|
||||
|
||||
### 11.3 הבדלים בין סוגי עררים
|
||||
|
||||
| מרכיב | ערר על היתר | ערר על תכנית | ערר היטל השבחה |
|
||||
|-------|------------|-------------|---------------|
|
||||
| בלוק ה (פתיחה) | "ערר על החלטת רשות הרישוי..." | "ערר על החלטת הוועדה..." | "ערר על שומת היטל השבחה..." |
|
||||
| בלוק ו (מהות) | בקשה להיתר — שטחים, קומות, הריסה | תכנית — סעיפי סמכות, שטחים | שומה — תכנית משביחה, לפני/אחרי |
|
||||
| בלוק ט (תכניות) | תמ"א 38, 10038, תכנית מפורטת | תכנית אב, תכנית קודמת | לא רלוונטי (בדרך כלל) |
|
||||
| ציטוט מרכזי ברקע | חוו"ד מחלקת שימור / מהנדס | החלטת ועדה מחוזית / פרוטוקול | שומה + שומה נגדית |
|
||||
| תמונות | תשריט היתר, הדמיות, סיור | תשריט תכנית, GIS, סיור | לרוב אין |
|
||||
|
||||
### 11.4 מיקומי תמונות מומלצים
|
||||
|
||||
| מיקום | מה להכניס | מתי |
|
||||
|-------|----------|-----|
|
||||
| אחרי תיאור מקרקעין (בלוק ו) | תשריט מיקום / מפת GIS עם סימון המגרש | תמיד |
|
||||
| אחרי מהות הבקשה (בלוק ו) | תשריט הבקשה / נספח בינוי / תכנית מוצעת | תמיד |
|
||||
| אחרי סביבת מקרקעין (בלוק ו) | צילום אוויר עם סימון המגרש והסביבה | מומלץ |
|
||||
| בתוך הליכים (בלוק ח) | צילומים מהסיור | אם היה סיור |
|
||||
| בתוך הליכים (בלוק ח) | הדמיות / חתכי בינוי מהשלמות טיעון | אם צורפו |
|
||||
| בתוך דיון (בלוק י) | המחשה סכמטית / השוואה לפני-אחרי | לפי הצורך |
|
||||
|
||||
כשמכינים את טיוטת המבנה (שלב 12 להלן), בכל מקום שמתוכננת תמונה יש להכניס תיבת טקסט עם:
|
||||
- "📷 תמונה:" + תיאור מה צריך להכניס
|
||||
- למשל: "📷 תמונה: תשריט מיקום המגרש מתוך מערכת GIS — לסמן את חלקה 244 באדום"
|
||||
|
||||
|
||||
### 11.5 פרמטרי עיבוד — סיכום מהיר
|
||||
|
||||
להגדרות מלאות של כל בלוק (content model, constraints, weight methodology, processing derivation) ראה `references/block-schema.md`. להלן טבלת סיכום בלבד:
|
||||
|
||||
| בלוק | Generation type | Temp | Thinking | Model |
|
||||
|------|----------------|------|----------|-------|
|
||||
| א-ד | template-fill | 0 | off | script |
|
||||
| ה | paraphrase | 0.2 | low | sonnet |
|
||||
| ו | reproduction | 0 | off | sonnet |
|
||||
| ז | paraphrase | 0.1 | low | sonnet |
|
||||
| ח | reproduction + paraphrase | 0 | off | sonnet |
|
||||
| ט | guided-synthesis | 0.2 | medium | opus |
|
||||
| **י** | **rhetorical-construction** | **0.4** | **max (16K+)** | **opus** |
|
||||
| יא | paraphrase | 0.1 | low | sonnet |
|
||||
| יב | template-fill | 0 | off | script |
|
||||
|
||||
|
||||
## 12. שלב מחייב: "טיוטת מבנה — לפני דיון"
|
||||
|
||||
### 12.1 מהות השלב
|
||||
|
||||
שלב זה מתבצע **אחרי** איסוף כל החומרים, המרת מסמכים, וניתוח טיעונים — ו**לפני** כתיבת פרק הדיון וההכרעה. מייצרים קובץ DOCX מעוצב עם כל חלקי ההחלטה **עד (ולא כולל)** פרק הדיון.
|
||||
|
||||
**כלל מחייב:** לעולם אל תכתוב פרק דיון והכרעה לפני שקובץ המבנה קיים ואושר.
|
||||
|
||||
### 12.1.1 כלל "ללא כפילות"
|
||||
|
||||
**כלל קריטי:** ככל שהרקע, הטענות וההליכים מפורטים יותר — כך הדיון צריך להיות קצר וממוקד יותר בניתוח בלבד. אין לחזור בדיון על עובדות או טענות שכבר פורטו. הדיון מניח שהקורא קרא את הרקע.
|
||||
|
||||
- **ברקע (בלוקים ה-ט):** פרט את כל העובדות, הציטוטים, הטענות, ההליכים — באופן מלא ומקיף.
|
||||
- **בדיון (בלוק י):** רק ניתוח, הכרעה, ומסקנות. הפנה לרקע ולטענות במקום לחזור עליהם: "כאמור בסעיף X לעיל...", "כפי שפורט...", "כפי שציינו...".
|
||||
- **כלל אצבע:** אם משפט בדיון חוזר על עובדה שכבר נאמרה ברקע מילה במילה — מחק אותו והחלף בהפניה.
|
||||
- **חריג:** חזרה מכוונת ("נשוב על כך כי...") היא כלי רטורי לגיטימי כשמוסיפים שכבה חדשה, לא כפילות.
|
||||
|
||||
### 12.2 מה כולל קובץ המבנה
|
||||
|
||||
הקובץ כולל את בלוקים א-ט ממולאים בתוכן, בלוק י (דיון) עם placeholder, בלוק יא (סיכום) עם placeholder, ובלוק יב (חתימות) מלא. תיבות תמונה מסומנות ב-"📷 תמונה:" במקומות הרלוונטיים.
|
||||
|
||||
### 12.3 סדר עבודה
|
||||
|
||||
```
|
||||
שלב 1: איסוף חומרים ← קריאת כל המסמכים
|
||||
שלב 2: המרת מסמכים ← PDF → Markdown
|
||||
שלב 3: ניתוח טיעונים ← זיהוי טענות, מיון, סיווג
|
||||
שלב 4: טיוטת מבנה ← ייצור DOCX עם בלוקים א-ט + placeholders ← שם הקובץ: החלטה-ערר-XXXX-מבנה.docx
|
||||
שלב 5: אישור המבנה ← דפנה מעיינת ומאשרת / מתקנת
|
||||
שלב 6: כתיבת דיון והכרעה ← בלוק י
|
||||
שלב 7: כתיבת סיכום ← בלוק יא
|
||||
שלב 8: עדכון DOCX סופי ← כל הבלוקים + תמונות אמיתיות
|
||||
```
|
||||
|
||||
### 12.4 עיצוב קובץ ה-DOCX
|
||||
|
||||
**חובה לקרוא קודם** את `.claude/skills/legal-docx/SKILL.md` — כל כללי RTL, פונטים, מידות וטבלאות מוגדרים שם. הסקריפט הבסיסי נמצא ב-`.claude/skills/legal-docx/scripts/create-legal-doc.js`.
|
||||
|
||||
כללי RTL קריטיים (מתוך legal-docx):
|
||||
- `AlignmentType.START` (לא LEFT/RIGHT) ליישור ימין
|
||||
- `AlignmentType.END` ליישור שמאל
|
||||
- שלוש הגדרות חובה: `bidi: true` (section), `bidirectional: true` (paragraph), `rightToLeft: true` (run)
|
||||
|
||||
להלן ההגדרות הייחודיות להחלטת ועדת ערר (מעבר למה שמוגדר ב-legal-docx):
|
||||
|
||||
| מרכיב | עיצוב | הערות |
|
||||
|-------|-------|-------|
|
||||
| כותרת "החלטה" | 16pt (32 half-points), bold, `AlignmentType.CENTER` | HeadingLevel.HEADING_1 |
|
||||
| כותרות פרקים ("תמצית טענות הצדדים", "דיון והכרעה") | 14pt (28 half-points), bold, קו תחתון, `AlignmentType.CENTER` | HeadingLevel.HEADING_2 |
|
||||
| כותרות משנה ("טענות העוררים") | 12pt (24 half-points), bold, `AlignmentType.CENTER`, ללא קו תחתון | פסקה רגילה עם bold |
|
||||
| מספור סעיפים | מספר + נקודה (bold, run נפרד) + טאב + טקסט (רגיל) | `alignment: AlignmentType.START`, hanging indent |
|
||||
| ציטוטים (blockquote) | הזחה 567 DXA (~1 ס"מ) משני הצדדים, פונט זהה לגוף | indent: { left: 567, right: 567 } |
|
||||
| תיבות תמונה | מסגרת עם shading אפור בהיר (fill: "F0F0F0"), טקסט "📷 תמונה: [תיאור]" | ShadingType.CLEAR |
|
||||
| חתימות | טבלה ללא גבולות (`visuallyRightToLeft: true`), 2 טורים | כמו בתבנית ב-create-legal-doc.js |
|
||||
| כותרת מוסדית | טבלה ללא גבולות, 2 טורים: ימין=מוסד, שמאל=מספרי תיק | `visuallyRightToLeft: true` |
|
||||
|
||||
|
||||
## 12. צ'קליסט תוכן לפי סוג ערר
|
||||
|
||||
> נוסף אפריל 2026 בעקבות ניתוח שיטתי של 24 החלטות. ראה: `docs/corpus-analysis.md`
|
||||
|
||||
הפרומפט של בלוק י מקבל **צ'קליסט תוכן** אוטומטי לפי סוג הערר (`lessons.py: CONTENT_CHECKLISTS`). זה מבטיח שהדיון יכסה את הנושאים הנדרשים — לא רק סגנון ומתודולוגיה, אלא תוכן ענייני.
|
||||
|
||||
### 12.1 חמישה תת-סוגי רישוי (לא שלושה)
|
||||
ניתוח הקורפוס חשף שלתיקי רישוי יש 5 תת-סוגים שונים מבחינת מבנה הדיון:
|
||||
|
||||
| תת-סוג | מה בדיון | דוגמאות |
|
||||
|---------|---------|---------|
|
||||
| **רישוי מהותי** | דיון תכנוני מקיף + משפטי | רוב ההחלטות |
|
||||
| **סף/סמכות** | משפטי בלבד, ללא תכנון | גבאי, ירושלים שקופה |
|
||||
| **קנייני** | תימוכין קנייניים, מינימום תכנון | טלי-אביב, הראל 1043 |
|
||||
| **תמ"א 38** | איזון אינטרסים + תכנון + שכנות | בית הכרם |
|
||||
| **שימוש חורג** | פרשנות תכניות מרובות | תורן |
|
||||
|
||||
### 12.2 דיון תכנוני — מתי ואיך
|
||||
**מתי חובה:** כשהערר מגיע לדיון מהותי (לא סף/סמכות, לא קנייני טהור).
|
||||
|
||||
**מבנה טיפוסי (מהקורפוס):**
|
||||
1. הקשר תכנוני רחב — תכניות חלות, ייעוד, סביבה (2-8 סעיפים)
|
||||
2. ציטוט ישיר מהוראות תכנית — בלוקים של 200-600 מילים עם "הדגשת הח"מ"
|
||||
3. יישום על המקרה — הוראה → עובדה → מסקנה
|
||||
4. מסקנה תכנונית — תואם/סוטה, מוצדק/לא
|
||||
|
||||
**נושאים שמופיעים בתדירות גבוהה:**
|
||||
- חניה (8/24 החלטות) — הנושא התכנוני הנפוץ ביותר, עומק של 5-15 סעיפים
|
||||
- קווי בניין (7/24) — כולל ניתוח סטייה ניכרת
|
||||
- ניתוח הוראות תכנית (18/24) — כמעט תמיד
|
||||
- פגיעה בשכנים (5/24) — צל, פרטיות, רעש
|
||||
|
||||
### 12.3 הערות יו"ר
|
||||
הערות דפנה על טיוטות מתועדות במערכת `chair_feedback` (DB + API + UI ב-`/feedback`). כל הערה מסווגת לקטגוריה ומפיקה לקח שמשפר את ההחלטות הבאות.
|
||||
574
skills/decision/references/block-schema.md
Normal file
574
skills/decision/references/block-schema.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Block Schema — ארכיטקטורת מסמך החלטת ועדת ערר
|
||||
|
||||
מסמך זה מגדיר את המבנה הפורמלי של החלטת ועדת ערר לתכנון ובניה. הוא משמש כמקור סמכותי להגדרת בלוקים, משקלות, פרמטרי עיבוד, וכללי ולידציה.
|
||||
|
||||
**הפניה:** SKILL.md סעיפים 11-12 מכילים סיכום מהיר והנחיות תהליך. מסמך זה מכיל את ההגדרות המלאות.
|
||||
|
||||
---
|
||||
|
||||
## 1. יסודות תיאורטיים
|
||||
|
||||
ארכיטקטורת המסמך מבוססת על שילוב של ארבעה frameworks מוכרים:
|
||||
|
||||
### CREAC — מתודולוגיית כתיבה משפטית
|
||||
Conclusion → Rule → Explanation → Application → Conclusion.
|
||||
מקור: Columbia Law School, Legal Writing methodology.
|
||||
**מיפוי:** חל על בלוק י (דיון) ובלוק יא (סיכום). בלוק י פותח במסקנה (C), מציג כלל משפטי (R), מסביר באמצעות פסיקה (E), מיישם על העובדות (A), וחוזר למסקנה (C). בלוק יא = C אחרון בלבד.
|
||||
|
||||
### Federal Judicial Center — Judicial Writing Manual
|
||||
מגדיר תפקוד פונקציונלי לכל חלק בהחלטה שיפוטית:
|
||||
- **Orientation** (אוריינטציה) — מי, מה, איפה → בלוקים א-ה
|
||||
- **Framing** (מסגור) — הקשר עובדתי ותכנוני → בלוק ו
|
||||
- **Argumentation** (טיעון) — עמדות הצדדים → בלוק ז
|
||||
- **Procedural record** (תיעוד הליכי) — מה עשינו → בלוק ח
|
||||
- **Deliberation** (דיון) — ניתוח משפטי → בלוקים ט-י
|
||||
- **Disposition** (החלטה) — תוצאה אופרטיבית → בלוק יא
|
||||
|
||||
### DITA — Darwin Information Typing Architecture
|
||||
סטנדרט OASIS להגדרת סוגי תוכן מובנים. מספק:
|
||||
- **Content model** — אילו אלמנטים מותרים בכל בלוק
|
||||
- **Constraints** — מה אסור (חשוב יותר ממה שמותר)
|
||||
- **Specialization** — ירושה מסוג בסיסי עם התאמות
|
||||
- **Relationships** — תלויות בין בלוקים
|
||||
|
||||
### Akoma Ntoso / LegalDocumentML
|
||||
סטנדרט OASIS בינלאומי למסמכים משפטיים מובנים (UN/DESA). מספק:
|
||||
- **Semantic mapping** — כל בלוק ממופה לרכיב מוכר בסטנדרט
|
||||
- **Document class** — "judgment" (פסק דין / החלטה)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. הגדרות בלוקים
|
||||
|
||||
### Block א: כותרת מוסדית / Institutional Header
|
||||
|
||||
**ID:** `block-alef`
|
||||
**Akoma Ntoso:** `meta > identification`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את המוסד, התיק והגורם המחליט.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: טבלה 2 טורים (מוסד | מספרי תיק)
|
||||
- Sources: מערכת ניהול תיקים
|
||||
|
||||
**Constraints:**
|
||||
- MUST: שם מוסד, מספר תיק, מספר תכנית/בקשה
|
||||
- MUST NOT: תוכן מהותי כלשהו
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע, לא משתנה בין סוגי עררים)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ב: הרכב הוועדה / Panel Composition
|
||||
|
||||
**ID:** `block-bet`
|
||||
**Akoma Ntoso:** `meta > references > TLCPerson`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את ההרכב המחליט. חשוב לביקורת שיפוטית (הרכב כשיר).
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: "בפני:" + יו"ר + חברים
|
||||
- Sources: מערכת ניהול
|
||||
|
||||
**Constraints:**
|
||||
- MUST: יו"ר + לפחות חבר אחד
|
||||
- MUST NOT: תוכן מהותי
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ג: צדדים / Parties
|
||||
|
||||
**ID:** `block-gimel`
|
||||
**Akoma Ntoso:** `meta > references > TLCPerson` (appellants, respondents)
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — מזהה את הצדדים וב"כ. מגדיר את מסגרת הדיון.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: עוררים + "נגד" + משיבים + ב"כ
|
||||
- Sources: כתב ערר, כתב תשובה
|
||||
|
||||
**Constraints:**
|
||||
- MUST: שם כל צד, "נגד" כמפריד
|
||||
- MUST NOT: תוכן מהותי, תיאור הערר
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ד: כותרת "החלטה" / Decision Title
|
||||
|
||||
**ID:** `block-dalet`
|
||||
**Akoma Ntoso:** `body > judgment > header`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Orientation — סימון פורמלי של תחילת ההחלטה.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: מילה אחת: "החלטה"
|
||||
- Sources: none
|
||||
|
||||
**Constraints:**
|
||||
- MUST: David 16pt, bold, מרכז
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 0% (שורה אחת)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
### Block ה: פתיחה / Opening
|
||||
|
||||
**ID:** `block-he`
|
||||
**Akoma Ntoso:** `body > judgment > introduction`
|
||||
**CREAC role:** C (מסקנה ראשונית — הצגת מה לפנינו)
|
||||
**Functional purpose (JWM):** Orientation — מכוון את הקורא למהות הערר במשפט אחד. מגדיר "להלן" מרכזיים.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative (1-2 סעיפים)
|
||||
- Elements: numbered-para עם הגדרות "להלן"
|
||||
- Sources: כתב ערר, החלטת ועדה מקומית
|
||||
|
||||
**Constraints:**
|
||||
- MUST: "לפנינו...", הגדרת הוועדה המקומית, הגדרת התכנית/הבקשה, הגדרת המגרש
|
||||
- MUST NOT: ניתוח, ערכי שיפוט, ציטוטים מצדדים
|
||||
- Dependencies: block-gimel (שמות צדדים להגדרות)
|
||||
|
||||
**Weight:** 1% (קבוע — 1-2 סעיפים)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase
|
||||
- Temperature: 0.2 | Thinking: low | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ו: רקע עובדתי / Factual Background ("פתח דבר")
|
||||
|
||||
**ID:** `block-vav`
|
||||
**Akoma Ntoso:** `body > judgment > background`
|
||||
**CREAC role:** none (עובדות בלבד, לא ניתוח)
|
||||
**Functional purpose (JWM):** Framing — מספק את התשתית העובדתית שעליה נבנה הדיון. השופט חייב להבין את המציאות בשטח לפני שקורא טענות.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block, image-placeholder
|
||||
- Elements: numbered-para, blockquote (ציטוט מפרוטוקול), image-box
|
||||
- Sources: כתבי טענות, תשריטים, פרוטוקולים, החלטות קודמות, GIS
|
||||
|
||||
**סדר תוכן פנימי:**
|
||||
1. מקרקעין — מיקום, שטח, מאפיינים
|
||||
2. סביבת מקרקעין — בנייה סמוכה, אופי
|
||||
3. 📷 תמונה: מיקום GIS
|
||||
4. היסטוריה תכנונית — תכניות, החלטות (עובדות יבשות בלבד)
|
||||
5. מהות הבקשה/תכנית
|
||||
6. 📷 תמונה: תשריט
|
||||
7. ציטוט מפרוטוקול ועדה מקומית
|
||||
8. החלטת הוועדה + תנאים
|
||||
9. 📷 תמונה: צילום אוויר (אופציונלי)
|
||||
10. הגשת הערר
|
||||
|
||||
**Constraints:**
|
||||
- MUST: מקרקעין, מהות הבקשה, החלטת הוועדה, הגשת הערר
|
||||
- MUST: לפחות 2 תמונות (מיקום + תשריט)
|
||||
- MUST: ציטוט מפרוטוקול הוועדה המקומית
|
||||
- ⚠️ **MUST NOT ("רקע ניטרלי"):** ציטוטים ישירים מצדדים, מילות ערך/שיפוט ("חריג", "חטא", "בעייתי"). החלטות קודמות = עובדה יבשה ("ביום X נדחתה תכנית Y"), ללא נימוקים וציטוטים מהן.
|
||||
- Dependencies: block-he (הגדרות "להלן")
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 15-25% | רקע מפורט עם הקשר תכנוני |
|
||||
| רישוי — קבלה | 30-40% | כולל ציטוט מפרוטוקול |
|
||||
| רישוי — קבלה חלקית | 25-35% | כולל ציטוט מפרוטוקול |
|
||||
| היטל השבחה | 6-18% | רקע מצומצם |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): גבוה — מספק את "התמונה" לשופט שלא מכיר את התיק
|
||||
- Reader attention (20%): בינוני-גבוה — primacy effect, הקורא קשוב בהתחלה
|
||||
- Judicial review (25%): גבוה — שופט בודק שהעובדות מלאות ומדויקות
|
||||
- Empirical (15%): מבוסס על מדידת החלטות דפנה (3.2 ב-SKILL.md)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: reproduction (העתקה נאמנה ממקורות)
|
||||
- Cognitive complexity: lookup (ארגון, לא ניתוח)
|
||||
- Accuracy: high-precision
|
||||
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ז: טענות הצדדים / Parties' Claims
|
||||
|
||||
**ID:** `block-zayin`
|
||||
**Akoma Ntoso:** `body > judgment > arguments`
|
||||
**CREAC role:** none (הצגת טענות, לא ניתוח)
|
||||
**Functional purpose (JWM):** Argumentation — מציג את עמדות הצדדים בנאמנות, כך שהקורא יבין את המחלוקת לפני שקורא את ההכרעה.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative
|
||||
- Elements: section-heading ("תמצית טענות הצדדים"), sub-headings (לכל צד), numbered-para
|
||||
- Sources: כתב ערר, כתב תשובה — **כתבי טענות מקוריים בלבד** (לא השלמות טיעון)
|
||||
|
||||
**סדר קבוע:**
|
||||
1. כותרת: "תמצית טענות הצדדים"
|
||||
2. "טענות העוררים" (אם כמה עוררים — תתי-כותרות לכל אחד)
|
||||
3. "עמדת הוועדה המקומית"
|
||||
4. "עמדת מבקשי ההיתר" / "עמדת מגישי התכנית"
|
||||
|
||||
**Constraints:**
|
||||
- MUST: כל טענה בסעיף נפרד, גוף שלישי ("העורר טוען כי...")
|
||||
- MUST: כל צד בפרק נפרד, סדר קבוע
|
||||
- MUST NOT: ניתוח, מסקנות, הערכת הוועדה ("טענה זו חלשה...")
|
||||
- MUST NOT: תוכן מהשלמות טיעון (→ block-chet)
|
||||
- Dependencies: block-vav (מספור רציף)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 30-40% | טענות מפורטות |
|
||||
| רישוי — קבלה | 20-30% | כולל השלמות |
|
||||
| רישוי — קבלה חלקית | 25-30% | |
|
||||
| היטל השבחה | 13-25% | |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): בינוני — הצגה, לא הכרעה
|
||||
- Reader attention (20%): נמוך-בינוני — scanning attention, הקורא מחפש טענות ספציפיות
|
||||
- Judicial review (25%): גבוה — שופט בודק ש"נשמעו כל הצדדים"
|
||||
- Empirical (15%): מבוסס על מדידת החלטות דפנה
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase (סיכום נאמן בשפה של דפנה)
|
||||
- Cognitive complexity: medium-synthesis (קיבוץ וסידור טענות)
|
||||
- Accuracy: high-precision (לא לפספס טענה, לא לעוות)
|
||||
- Temperature: 0.1 | Thinking: low | Effort: medium | Model: sonnet
|
||||
|
||||
|
||||
### Block ח: הליכים בפני ועדת הערר / Proceedings
|
||||
|
||||
**ID:** `block-chet`
|
||||
**Akoma Ntoso:** `body > judgment > proceedings` (custom extension)
|
||||
**CREAC role:** none (תיעוד, לא ניתוח)
|
||||
**Functional purpose (JWM):** Procedural record — מתעד שהוועדה פעלה כדין ונתנה מלוא יום בבית דין. קריטי ל"מבחן השופט" — שופט בעתמ"ם בודק שהצדדים קיבלו הזדמנות הוגנת.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, image-placeholder
|
||||
- Elements: section-heading ("ההליכים בפני ועדת הערר"), numbered-para, image-box
|
||||
- Sources: פרוטוקול דיון, תמונות סיור, החלטות ביניים, השלמות טיעון
|
||||
|
||||
**סדר כרונולוגי:**
|
||||
1. דיון — תאריך, נוכחים
|
||||
2. סיור — תאריך, תיאור
|
||||
3. 📷 תמונה: צילומים מהסיור
|
||||
4. השלמות טיעון — עם תוכן מפורט (כל השלמה = סעיף נפרד)
|
||||
5. החלטות ביניים
|
||||
6. תגובות לתגובות — כרונולוגי
|
||||
7. 📷 תמונה: הדמיות/חתכים (אם צורפו)
|
||||
8. עררים מקבילים (אם יש)
|
||||
|
||||
**Constraints:**
|
||||
- MUST: תאריכים מדויקים, כרונולוגיה ברורה
|
||||
- MUST: תוכן השלמות טיעון מפורט — כל השלמה בסעיף נפרד עם תמצית תוכן
|
||||
- MUST NOT: ניתוח או הערכה של ההשלמות ("טענה חזקה/חלשה")
|
||||
- Dependencies: block-zayin (מספור רציף)
|
||||
- References: block-zayin (הפניה לטענות מקוריות כשיש חפיפה)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| ערר פשוט (ללא השלמות) | 3-5% | דיון + סיור בלבד |
|
||||
| ערר מורכב (השלמות רבות) | 8-15% | כמו אריאלי: 31 סעיפים |
|
||||
| היטל השבחה | 2-4% | בדרך כלל מינימלי |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): נמוך-בינוני — תיעוד, לא הכרעה
|
||||
- Reader attention (20%): נמוך — scanning, אלא אם יש ממצאים חדשים מסיור/השלמות
|
||||
- Judicial review (25%): **גבוה מאוד** — שופט בודק שנתנו procedural fairness
|
||||
- Empirical (15%): מגוון רחב — תלוי בכמות ההשלמות
|
||||
|
||||
**Processing:**
|
||||
- Generation type: reproduction + paraphrase (תאריכים מדויקים + תמצית תוכן)
|
||||
- Cognitive complexity: low (סידור כרונולוגי)
|
||||
- Accuracy: high-precision (תאריכים, שמות מסמכים)
|
||||
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block ט: תכניות חלות / Applicable Plans (אופציונלי)
|
||||
|
||||
**ID:** `block-tet`
|
||||
**Akoma Ntoso:** `body > judgment > motivation > background` (extended)
|
||||
**CREAC role:** R (Rule — הצגת הכללים המשפטיים/תכנוניים)
|
||||
**Functional purpose (JWM):** Deliberation (preliminary) — מציג את המסגרת הנורמטיבית שלאורה ייבחנו הטענות. בלוק גשר בין עובדות לניתוח.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block
|
||||
- Elements: section-heading, numbered-para, blockquote (ציטוט מהוראות תכנית)
|
||||
- Sources: הוראות תכנית (PDF), נספחי בינוי, החלטות מרכזות
|
||||
|
||||
**Constraints:**
|
||||
- MUST: ציטוט ישיר מהוראות תכנית עם הדגשת (bold) מילים מכריעות
|
||||
- MUST NOT: ניתוח מעמיק (→ block-yod), הכרעה בין פרשנויות
|
||||
- Dependencies: block-chet (מספור), block-vav (הגדרות תכניות)
|
||||
- Condition: **אופציונלי** — רק כשיש מורכבות תכנונית (תכניות סותרות, תמ"א 38 + שימור, פרשנות)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| מתי קיים | משקל |
|
||||
|----------|------|
|
||||
| תמ"א 38 + שימור | 8-12% |
|
||||
| פרשנות תכנית | 5-10% |
|
||||
| לא קיים | 0% |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): בינוני — הנחת תשתית נורמטיבית
|
||||
- Reader attention (20%): נמוך — טכני, אלא אם פרשנות שנויה במחלוקת
|
||||
- Judicial review (25%): בינוני — שופט בודק שהוועדה הבינה את הדין
|
||||
- Empirical (15%): אריאלי — 14 סעיפים; בית הכרם — משולב בדיון
|
||||
|
||||
**Processing:**
|
||||
- Generation type: guided-synthesis (ציטוט + ניתוח ראשוני)
|
||||
- Cognitive complexity: medium (פרשנות טקסט משפטי)
|
||||
- Accuracy: precision + interpretation
|
||||
- Temperature: 0.2 | Thinking: medium | Effort: medium | Model: opus
|
||||
|
||||
|
||||
### Block י: דיון והכרעה / Discussion and Decision
|
||||
|
||||
**ID:** `block-yod`
|
||||
**Akoma Ntoso:** `body > judgment > motivation`
|
||||
**CREAC role:** **full-CREAC** — C (מסקנה בפתיחה) → R (כלל משפטי) → E (ציטוט פסיקה) → A (יישום על העובדות) → C (מסקנת ביניים)
|
||||
**Functional purpose (JWM):** Deliberation — ליבת ההחלטה. כאן הוועדה מנתחת, מאזנת, ומכריעה. זהו ה-ratio decidendi.
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative, citation-block, image-placeholder
|
||||
- Elements: numbered-para (אסה רציפה ללא כותרות משנה), blockquote (ציטוטי פסיקה ותכנית), image-box
|
||||
- Sources: **כל** הבלוקים הקודמים + פסיקה + skill
|
||||
|
||||
**מבנה פנימי (לפי סוג ערר — ראה SKILL.md סעיף 7.3):**
|
||||
- דחייה: שכבות הגנה (concentric circles)
|
||||
- קבלה: נימוק-נימוק
|
||||
- קבלה חלקית: מיפוי מתחים + ניתוח נושאי
|
||||
- היטל השבחה: פתיחה ישירה עם מסקנה
|
||||
|
||||
**Constraints:**
|
||||
- MUST: מסקנה בפתיחת הדיון (לא בסוף)
|
||||
- MUST: מענה לכל טענה שהוצגה בבלוק ז
|
||||
- MUST: ציטוט פסיקה בבלוקים ארוכים (200-600 מילים)
|
||||
- ⚠️ **MUST NOT ("ללא כפילות"):** חזרה על עובדות/טענות מבלוקים קודמים. השתמש בהפניות: "כאמור בסעיף X לעיל", "כפי שפורט", "כפי שציינו"
|
||||
- MUST NOT: כותרות משנה (חריג: נושאים נפרדים לחלוטין)
|
||||
- Dependencies: **ALL** previous blocks (ה-ט)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל | הערות |
|
||||
|---------|------|-------|
|
||||
| רישוי — דחייה | 37-50% | פתיחה רחבה + שכבות |
|
||||
| רישוי — קבלה | 35-45% | נימוק-נימוק |
|
||||
| רישוי — קבלה חלקית | 40-47% | מיפוי מתחים + ניתוח נושאי |
|
||||
| היטל השבחה | 32-48% | ציטוטי פסיקה מרובים |
|
||||
|
||||
**Weight methodology:**
|
||||
- Communicative weight (40%): **מקסימלי** — זהו ה-ratio decidendi, תכלית ההחלטה
|
||||
- Reader attention (20%): **גבוה** — deep reading, הקורא מחפש את הנימוקים
|
||||
- Judicial review (25%): **מקסימלי** — שופט בוחן סבירות, מידתיות, התייחסות לטענות
|
||||
- Empirical (15%): 35-50% באופן עקבי בכל החלטות דפנה
|
||||
|
||||
**Processing:**
|
||||
- Generation type: **rhetorical-construction** (בניית טיעון, איזון, רטוריקה)
|
||||
- Cognitive complexity: **high-reasoning** (CREAC מלא, שכבות, חידוד)
|
||||
- Accuracy: **precision + creativity** (ניתוח מדויק + ביטוי אלגנטי)
|
||||
- Temperature: **0.4** | Thinking: **max (budget 16K+)** | Effort: **max** | Model: **opus בלבד**
|
||||
|
||||
|
||||
### Block יא: סיכום / סוף דבר / Summary
|
||||
|
||||
**ID:** `block-yod-alef`
|
||||
**Akoma Ntoso:** `body > judgment > decision`
|
||||
**CREAC role:** C (Conclusion אחרון — תמצית אופרטיבית)
|
||||
**Functional purpose (JWM):** Disposition — ההוראה האופרטיבית שמבצעים. זה מה שהצדדים צריכים לדעת "מה עכשיו."
|
||||
|
||||
**Content model:**
|
||||
- Types: narrative
|
||||
- Elements: section-heading ("סיכום"/"סוף דבר"), numbered-para, sub-items (א. ב. ג.)
|
||||
- Sources: block-yod (מסקנות)
|
||||
|
||||
**מבנה לפי תוצאה (ראה SKILL.md סעיף 8):**
|
||||
- דחייה: "הערר נדחה" + תתי-סעיפים + פסקה חמה (רישוי בלבד)
|
||||
- קבלה: "הערר מתקבל בכפוף ל..." + פרוזה
|
||||
- קבלה חלקית: "הערר מתקבל באופן חלקי" + 2-3 הוראות אופרטיביות
|
||||
- היטל השבחה: יבש
|
||||
|
||||
**Constraints:**
|
||||
- MUST: תוצאה ברורה (נדחה/מתקבל/מתקבל חלקית)
|
||||
- MUST NOT (בקבלה חלקית): חזרה על נימוקים — ההנמקה כבר בדיון
|
||||
- Dependencies: block-yod (מסקנות)
|
||||
|
||||
**Weight:**
|
||||
|
||||
| סוג ערר | משקל |
|
||||
|---------|------|
|
||||
| דחייה | 2-9% |
|
||||
| קבלה | 3-5% |
|
||||
| קבלה חלקית | 2-3% |
|
||||
| היטל השבחה | 3-4% |
|
||||
|
||||
**Processing:**
|
||||
- Generation type: paraphrase (עיבוד מסקנות בלוק י)
|
||||
- Cognitive complexity: low
|
||||
- Accuracy: high-precision (הוראות חייבות להיות חד-משמעיות)
|
||||
- Temperature: 0.1 | Thinking: low | Effort: low | Model: sonnet
|
||||
|
||||
|
||||
### Block יב: חתימות / Signatures
|
||||
|
||||
**ID:** `block-yod-bet`
|
||||
**Akoma Ntoso:** `conclusions > signature`
|
||||
**CREAC role:** none
|
||||
**Functional purpose (JWM):** Authentication — אישור פורמלי של ההחלטה.
|
||||
|
||||
**Content model:**
|
||||
- Types: template-field
|
||||
- Elements: "ניתנה פה אחד" + תאריך עברי/לועזי + טבלת חתימות
|
||||
- Sources: none
|
||||
|
||||
**Constraints:**
|
||||
- MUST: "ניתנה פה אחד", תאריך, יו"ר + מזכיר/ה
|
||||
- Dependencies: none
|
||||
|
||||
**Weight:** 1% (קבוע)
|
||||
|
||||
**Processing:**
|
||||
- Generation type: template-fill
|
||||
- Temperature: 0 | Thinking: off | Effort: min | Model: script
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. כללי גזירת פרמטרים
|
||||
|
||||
פרמטרי העיבוד נגזרים ממאפייני התוכן, לא נקבעים שרירותית:
|
||||
|
||||
### Temperature — נגזר מסוג הייצור
|
||||
|
||||
| Generation type | Temperature | נימוק |
|
||||
|----------------|-------------|-------|
|
||||
| template-fill | 0 | אין צורך בשפה — מילוי שדות |
|
||||
| reproduction | 0 | נאמנות מוחלטת למקור. אפס יצירתיות |
|
||||
| paraphrase | 0.1 | מרווח מינימלי לניסוח בשפה של דפנה |
|
||||
| guided-synthesis | 0.2 | גמישות בארגון וחיבור מקורות, לא בתוכן |
|
||||
| analytical-reasoning | 0.3-0.4 | צריך ליצור קשרים בין עקרונות משפטיים |
|
||||
| rhetorical-construction | 0.4-0.5 | טווח ביטוי רחב לכתיבה משכנעת ואלגנטית |
|
||||
|
||||
### Thinking budget — נגזר ממורכבות קוגניטיבית
|
||||
|
||||
| Cognitive task | Budget | נימוק |
|
||||
|---------------|--------|-------|
|
||||
| template-fill / lookup | off | אין צורך בחשיבה |
|
||||
| sequential-extraction | low | חילוץ מידע חד-שלבי |
|
||||
| multi-source-integration | medium | צריך להצליב מקורות |
|
||||
| legal-analysis-with-CREAC | max (16K+) | חשיבה רב-שלבית: מסקנה → כלל → הסבר → יישום |
|
||||
|
||||
### Model — נגזר מדרישת דיוק
|
||||
|
||||
| Accuracy profile | Model | נימוק |
|
||||
|-----------------|-------|-------|
|
||||
| factual-precision | sonnet | מהיר, מדויק לחילוץ עובדות |
|
||||
| precision + interpretation | opus | נדרש לפרשנות תכנית / ציטוט מובנה |
|
||||
| precision + creativity | opus | נדרש לניתוח משפטי מורכב ורטוריקה |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. מתודולוגיית משקלות
|
||||
|
||||
משקל כל בלוק נקבע על ידי שקלול 4 גורמים:
|
||||
|
||||
### 4.1 Communicative Weight (40%)
|
||||
מה חלקו של הבלוק בתכלית ההחלטה? ההחלטה באה לעשות דבר אחד: להכריע במחלוקת ולנמק. בלוק י (דיון) הוא ליבת התכלית. בלוקים א-ד (כותרות) הם עטיפה.
|
||||
|
||||
### 4.2 Reader Attention Distribution (20%)
|
||||
מבוסס על מחקרי F-pattern ו-primacy/recency:
|
||||
- **פתיחה** (בלוקים ה-ו): קשב גבוה (primacy effect)
|
||||
- **אמצע** (בלוקים ז-ח): scanning — הקורא מחפש טענות ספציפיות
|
||||
- **דיון** (בלוק י): deep reading — הקורא מחפש נימוקים
|
||||
- **סיום** (בלוק יא): קשב גבוה (recency effect)
|
||||
|
||||
### 4.3 Judicial Review Requirement (25%)
|
||||
מה שופט בבית משפט לעניינים מנהליים יבדוק ("מבחן השופט"):
|
||||
- **תשתית עובדתית** (בלוק ו): מלאה ומדויקת?
|
||||
- **שמיעת צדדים** (בלוקים ז-ח): נתנו מלוא יום בבית דין?
|
||||
- **סבירות ומידתיות** (בלוק י): ההכרעה מנומקת ומאוזנת?
|
||||
- **התייחסות לטענות** (בלוק י): כל טענה קיבלה מענה?
|
||||
|
||||
### 4.4 Empirical Basis (15%)
|
||||
מבוסס על מדידה מהחלטות שפורסמו:
|
||||
- הכט 1180-1181 (דחייה, 02.2026)
|
||||
- בית הכרם 1126/25 (קבלה חלקית, 03.2026)
|
||||
- אריאלי 1078+1083 (קבלה, 03.2026)
|
||||
|
||||
המשקלות ב-SKILL.md סעיף 3.2 (יחסי הזהב) משמשים כבסיס אמפירי שאומת על ידי שלושת הגורמים האנליטיים.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. כללי ולידציה
|
||||
|
||||
### 5.1 סדר בלוקים
|
||||
- בלוקים חייבים להופיע בסדר א עד יב
|
||||
- בלוקים א-ה ויב נדרשים בכל החלטה
|
||||
- בלוק ט אופציונלי (רק כשיש מורכבות תכנונית)
|
||||
|
||||
### 5.2 Content Constraints
|
||||
- **רקע ניטרלי (בלוק ו):** אם סעיף מכיל ציטוט ישיר מצד או מילת שיפוט → לא שייך כאן
|
||||
- **טענות מקוריות בלבד (בלוק ז):** רק מכתבי ערר/תשובה. השלמות → בלוק ח
|
||||
- **ללא כפילות (בלוק י):** הפניה לבלוקים קודמים, לא חזרה. חריג: "נשוב על כך כי..." (חזרה מכוונת עם שכבה חדשה)
|
||||
- **הליכים ללא הערכה (בלוק ח):** תיעוד מה הוגש, לא הערכה של חוזק הטענות
|
||||
|
||||
### 5.3 Weight Compliance
|
||||
- משקל כל בלוק (ספירת מילים / סה"כ) צריך להיות בטווח המוגדר **±10%**
|
||||
- אם בלוק י < 30% → flag: דיון לא מפותח מספיק
|
||||
- אם בלוק ו > 35% → flag: רקע מנופח, בדוק שאין תוכן טענתי
|
||||
|
||||
### 5.4 Structural Integrity
|
||||
- מספור סעיפים רציף מ-1 עד הסוף, ללא איפוס בין בלוקים
|
||||
- כל הגדרת "להלן" חייבת להופיע לפני השימוש הראשון בה
|
||||
- כל טענה בבלוק ז חייבת לקבל מענה בבלוק י (ישיר או "למעלה מן הצורך")
|
||||
- כותרות פרקים: David 14pt, bold, קו תחתון, מרכז
|
||||
- כותרות משנה: David 12pt, bold, מרכז, ללא קו תחתון
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. גרף תלויות בין בלוקים
|
||||
|
||||
```
|
||||
א (כותרת) → עצמאי
|
||||
ב (הרכב) → עצמאי
|
||||
ג (צדדים) → עצמאי
|
||||
ד (כותרת) → עצמאי
|
||||
ה (פתיחה) → תלוי ב: ג (שמות צדדים להגדרות "להלן")
|
||||
ו (רקע) → תלוי ב: ה (הגדרות). מספור ממשיך מ-ה.
|
||||
ז (טענות) → תלוי ב: ו (מספור). מפנה ל: ה, ו (הגדרות)
|
||||
ח (הליכים) → תלוי ב: ז (מספור). מפנה ל: ז (טענות מקוריות)
|
||||
ט (תכניות) → תלוי ב: ח (מספור). אופציונלי. מפנה ל: ו (הגדרות תכניות)
|
||||
י (דיון) → תלוי ב: **כל** הבלוקים ה-ט. מפנה ל: כולם.
|
||||
יא (סיכום) → תלוי ב: י (מסקנות). מפנה ל: י בלבד.
|
||||
יב (חתימות) → עצמאי
|
||||
```
|
||||
97
skills/decision/references/decision-template.json
Normal file
97
skills/decision/references/decision-template.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"_comment": "תבנית JSON לייצור טיוטת מבנה החלטה — כל שדה עם '_comment' הוא הסבר, לא תוכן",
|
||||
"_usage": "node .claude/skills/legal-decision/scripts/create-decision-structure.cjs <this-file.json> [output.docx]",
|
||||
|
||||
"type": "permit | plan | betterment",
|
||||
|
||||
"case_numbers": ["1130/25"],
|
||||
"plan_number": "152-1257682",
|
||||
"request_number": null,
|
||||
|
||||
"panel": {
|
||||
"chair": "עו\"ד דפנה תמיר",
|
||||
"members": [
|
||||
"יעקב אנוקה, נציג מתכנן המחוז",
|
||||
"יצחק ליר ליפובצקי, נציג ארגון האדריכלים"
|
||||
]
|
||||
},
|
||||
|
||||
"appellants": [
|
||||
{ "name": "1. מרק קובר", "representative": null },
|
||||
{ "name": "2. יצחק מטמון", "representative": null }
|
||||
],
|
||||
|
||||
"respondents": [
|
||||
{ "name": "הוועדה המרחבית לתכנון ולבניה הראל", "representative": "עו\"ד ליאורה אפרתי" },
|
||||
{ "name": "אסתר שרה ליבמן", "representative": null },
|
||||
{ "name": "יוסף חיים ליבמן", "representative": "עו\"ד משה ברקוביץ" }
|
||||
],
|
||||
|
||||
"appeal_description": "ערר על החלטת הוועדה המרחבית לתכנון ולבניה הראל מיום 23.7.2025, לדחות את ההתנגדויות ולאשר את תכנית מס' 152-1257682",
|
||||
|
||||
"opening_paragraphs": [
|
||||
"לפנינו ערר על החלטת הוועדה המרחבית לתכנון ולבניה הראל (להלן: \"הוועדה המקומית\") מיום 23.7.2025, לדחות את ההתנגדויות ולאשר את תכנית מס' 152-1257682 (להלן: \"התכנית\") ברחוב אבינדב 23, קריית יערים, גוש 29536 חלקה 244 (להלן: \"המגרש\" או \"המקרקעין\")."
|
||||
],
|
||||
|
||||
"use_petach_davar": true,
|
||||
|
||||
"property_description": "המגרש מצוי ברחוב אבינדב בקריית יערים, מועצה מקומית המוגדרת בתמ\"א 35 כ\"ישוב שמור משולב\" ובתמ\"מ 30/1 כ\"ישוב פרברי\". רחוב אבינדב הינו מדרחוב צר ומפותל, מתאפיין בבנייה כפרית של וילות צמודות קרקע חד-משפחתיות, עם חניה בתוך תחום המגרשים, על פי תכנית מי/135 משנת 1972 (להלן: \"תכנית 135\").",
|
||||
|
||||
"planning_history": [
|
||||
"בשנת 1992 אושרה תכנית מי/135/א (להלן: \"תכנית 135/א\"), אשר שינתה מהותית את ייעוד מספר מגרשים ברחוב...",
|
||||
"ביום 29.6.2017 דחתה ועדת המשנה להתנגדויות של הוועדה המחוזית ירושלים את תכנית מס' 152-0137067..."
|
||||
],
|
||||
|
||||
"request_essence": "התכנית נשוא הערר (152-1257682) עוסקת בתוספת זכויות בנייה למבנה הקיים ברחוב אבינדב 23, וכוללת: הגדלת זכויות בנייה בשיעור של 50%...",
|
||||
|
||||
"committee_protocol_quote": "[ציטוט מלא מתוך פרוטוקול הדיון בוועדה המקומית]",
|
||||
|
||||
"committee_decision": "ביום 23.7.2025 קיימה הוועדה המקומית דיון בהתנגדויות שהוגשו לתכנית. לאחר שמיעת ההתנגדויות, החליטה הוועדה המקומית פה אחד לדחות את ההתנגדויות ולאשר את התכנית כפי שהופקדה.",
|
||||
|
||||
"include_aerial_photo": true,
|
||||
|
||||
"surroundings": "[תיאור סביבת המקרקעין — מבנים סמוכים, אופי שכונתי]",
|
||||
|
||||
"appeal_filing": "ביום 14.8.2025 הגיש העורר את הערר שבפנינו. ביום 22.10.2025 הגיש מטמון כתב ערר/תשובה בו הצטרף לערר.",
|
||||
|
||||
"_comment_claims": "אפשר להשתמש ב-appellant_claims (רשימה אחת) או ב-appellant_claims_sections (כמה עוררים עם כותרות נפרדות)",
|
||||
|
||||
"appellant_claims_sections": [
|
||||
{
|
||||
"title": "טענות העורר מר קובר",
|
||||
"claims": [
|
||||
"העורר טוען כי לוועדה המקומית לא הייתה סמכות לאשר את התכנית...",
|
||||
"עוד טוען העורר כי חישוב תוספת שטחי הבנייה שגוי..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "טענות מטמון",
|
||||
"claims": [
|
||||
"מטמון מדגיש את העיוות התכנוני ההיסטורי שנוצר בשנת 1992..."
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"committee_position": [
|
||||
"הוועדה המקומית טוענת כי לוועדה מקומית סמכות לדון בתכנית...",
|
||||
"באשר להחלטת הוועדה המחוזית משנת 2017, טוענת הוועדה המקומית כי..."
|
||||
],
|
||||
|
||||
"applicant_position": [
|
||||
"מגישי התכנית טוענים כי התכנית אינה מוסיפה יחידות דיור מעבר למותר...",
|
||||
"במסגרת השלמת הטיעון, הציגו מגישי התכנית תמונה תכנונית רחבה..."
|
||||
],
|
||||
|
||||
"proceedings": [
|
||||
{ "type": "text", "text": "ביום 27.10.2025 התקיים דיון בפני ועדת הערר, בו נשמעו העורר, מטמון, הוועדה המקומית ומגישי התכנית." },
|
||||
{ "type": "text", "text": "ביום 30.11.2025 נערך סיור במקום." },
|
||||
{ "type": "image", "description": "צילומים מהסיור ברחוב אבינדב — מבט על הבניין הקיים" },
|
||||
{ "type": "text", "text": "ביום 31.12.2025 ניתנה החלטת ביניים..." },
|
||||
{ "type": "text", "text": "בעקבות החלטת הביניים הוגשו השלמות טיעון מכל הצדדים..." }
|
||||
],
|
||||
|
||||
"skip_plans_section": true,
|
||||
"_comment_plans": "אם skip_plans_section=false, אפשר להוסיף applicable_plans עם פירוט תכניות",
|
||||
|
||||
"secretary": ""
|
||||
}
|
||||
186
skills/decision/references/israeli-planning-law-basics.md
Normal file
186
skills/decision/references/israeli-planning-law-basics.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# יסודות דיני תכנון ובניה בישראל
|
||||
## מדריך עזר למומחה הגישה השיפוטית
|
||||
|
||||
---
|
||||
|
||||
## מבנה מערכת התכנון והבניה בישראל
|
||||
|
||||
### רמות התכנון
|
||||
|
||||
**1. רמה ארצית**
|
||||
- המועצה הארצית לתכנון ובניה
|
||||
- תכניות מתאר ארציות (תמ"א)
|
||||
|
||||
**2. רמה מחוזית**
|
||||
- ועדות מחוזיות (6 מחוזות)
|
||||
- תכניות מתאר מחוזיות (תמ"מ)
|
||||
|
||||
**3. רמה מקומית**
|
||||
- ועדות מקומיות (לפי רשויות מקומיות)
|
||||
- תכניות מפורטות
|
||||
|
||||
**4. ועדות ערר**
|
||||
- ועדות ערר מחוזיות (מטפלות בערעורים על החלטות ועדות מקומיות)
|
||||
- ועדת הערר הארצית (מטפלת בערעורים על החלטות ועדות מחוזיות)
|
||||
|
||||
---
|
||||
|
||||
## חוקים ותקנות מרכזיים
|
||||
|
||||
### 1. חוק התכנון והבניה, התשכ"ה-1965
|
||||
|
||||
**סעיפים חשובים:**
|
||||
|
||||
- **סעיף 19** - היטל השבחה
|
||||
- **סעיף 62** - הוועדה המקומית
|
||||
- **סעיף 149** - היתר בניה
|
||||
- **סעיף 150** - תנאים למתן היתר
|
||||
- **סעיף 151** - היתר בנייה בסטייה מזניחה
|
||||
- **סעיף 197** - הקלות בניה
|
||||
- **סעיף 204** - ועדות ערר
|
||||
- **סעיף 205** - סמכויות ועדת הערר
|
||||
- **סעיף 228** - עבירות בניה ללא היתר
|
||||
|
||||
### 2. חוק המקרקעין, התשכ"ט-1969
|
||||
|
||||
**סעיפים חשובים:**
|
||||
- זכויות קנייניות במקרקעין
|
||||
- העברת זכויות
|
||||
|
||||
### 3. תקנות התכנון והבניה (בקשה להיתר, תנאיו ואגרות), התש"ל-1970
|
||||
|
||||
---
|
||||
|
||||
## סוגי ערעורים נפוצים
|
||||
|
||||
### 1. ערעורים על החלטות בעניין רישוי
|
||||
- דחיית בקשה להיתר בניה
|
||||
- מתן היתר בתנאים
|
||||
- היתר בסטייה מזניחה
|
||||
- הקלות בניה
|
||||
|
||||
### 2. ערעורים על החלטות בעניין היטל השבחה
|
||||
- חישוב היטל
|
||||
- שיעור ההשבחה
|
||||
- פטור מהיטל
|
||||
- ערעורי שומה
|
||||
|
||||
### 3. ערעורים על החלטות בעניין תכניות
|
||||
- אישור/דחיית תכנית
|
||||
- תנאים בתכנית
|
||||
- התנגדויות לתכניות
|
||||
|
||||
### 4. ערעורים על צווי הריסה
|
||||
- צווים מנהליים
|
||||
- הליכי אכיפה
|
||||
|
||||
---
|
||||
|
||||
## פסקי דין חשובים בתחום
|
||||
|
||||
### בנושא היטל השבחה
|
||||
- **ע"א 6339/97 עיריית רמת השרון נ' פנחס** - עקרונות יסוד בהיטל השבחה
|
||||
- **ע"א 2892/07 בן ארי נ' הוועדה המקומית** - חישוב שיעור השבחה
|
||||
|
||||
### בנושא רישוי והיתרים
|
||||
- **בג"ץ 5060/94 ד"ר ניימן נ' יו"ר ועדת הערר** - גבולות שיקול הדעת של ועדות
|
||||
- **ע"א 1845/04 אושרי נ' הוועדה המקומית** - הקלות בניה
|
||||
|
||||
### בנושא שימוש חורג
|
||||
- **בג"ץ 244/00 עמותת יער חמ"ד נ' ועדת הערר** - שימושים חורגים
|
||||
|
||||
### בנושא זכויות קנייניות מול אינטרס ציבורי
|
||||
- **בג"ץ 6698/95 קעדאן נ' מינהל מקרקעי ישראל** - איזון בין זכויות
|
||||
- **בג"ץ 2390/96 קרסיק נ' הממשלה** - זכות הקניין
|
||||
|
||||
---
|
||||
|
||||
## מושגי יסוד
|
||||
|
||||
### תכנון
|
||||
|
||||
**תכנית מתאר** - תכנית כללית לשטח מסוים, קובעת ייעודי קרקע, צפיפות, זכויות בניה.
|
||||
|
||||
**תכנית מפורטת** - תכנית המפרטת פרטי בניה, קווי בניין, גבהים.
|
||||
|
||||
**שימוש חורג** - שימוש שונה מהייעוד בתכנית.
|
||||
|
||||
**הקלה** - סטייה קלה מתכנית שניתן לאשר ללא שינוי התכנית (סעיף 197).
|
||||
|
||||
**סטייה מזניחה** - סטייה קלה מאוד מהיתר שניתן לאשר (סעיף 151).
|
||||
|
||||
### רישוי
|
||||
|
||||
**היתר בניה** - אישור לבנות על פי תכנית.
|
||||
|
||||
**תנאים להיתר** - דרישות שיש לעמוד בהן לפני מתן היתר או במהלך הבניה.
|
||||
|
||||
**פקיעת היתר** - תום תוקף היתר אם לא הוחל בבניה.
|
||||
|
||||
### היטל השבחה
|
||||
|
||||
**השבחה** - עליית ערך מקרקעין כתוצאה מתכנית או היתר.
|
||||
|
||||
**היטל השבחה** - תשלום חובה בשיעור של 50% מההשבחה (בדרך כלל).
|
||||
|
||||
**מימוש זכויות** - התחלת בנייה או מכירה המחייבת בתשלום היטל.
|
||||
|
||||
**פטור מהיטל** - מקרים בהם החוק פוטר מתשלום (למשל: בניית דירת מגורים יחידה).
|
||||
|
||||
### הליכים
|
||||
|
||||
**ערר** - ערעור על החלטת ועדה מקומית לועדת ערר מחוזית.
|
||||
|
||||
**ערעור מינהלי** - ערעור מועדת ערר לבית משפט.
|
||||
|
||||
**בקשה מנהלית** - פנייה לביה"מ ללא צורך בערר קודם (במקרים מסוימים).
|
||||
|
||||
---
|
||||
|
||||
## סמכויות ועדת הערר (סעיף 205)
|
||||
|
||||
ועדת ערר רשאית:
|
||||
1. לאשר את ההחלטה
|
||||
2. לבטל את ההחלטה
|
||||
3. להורות על שינוי בהחלטה
|
||||
4. להחזיר לועדה המקומית לדיון נוסף
|
||||
|
||||
ועדת הערר פועלת כערכאה מינהלית וסמכויותיה דומות לאלו של בית משפט.
|
||||
|
||||
---
|
||||
|
||||
## עקרונות פסיקתיים נפוצים
|
||||
|
||||
### 1. שיקול דעת מנהלי
|
||||
- לועדות תכנון יש שיקול דעת רחב
|
||||
- בית המשפט (וועדת הערר) יתערב רק אם השיקול לא סביר, שרירותי, או בחוסר סמכות
|
||||
|
||||
### 2. האיזון בין זכויות קנייניות לאינטרס ציבורי
|
||||
- יש לאזן בין זכות הקניין של הפרט לבין האינטרס הציבורי
|
||||
- שני הערכים חשובים ויש לשקלם
|
||||
|
||||
### 3. ההסתמכות על מומחים
|
||||
- חוות דעת מומחה חשובה אך לא קובעת
|
||||
- הועדה רשאית לסטות מחוות דעת אם תנמק
|
||||
|
||||
### 4. הנמקת החלטות
|
||||
- כל החלטה חייבת להיות מנומקת
|
||||
- חוסר נימוק או נימוק לקוני עלול להביא לביטול ההחלטה
|
||||
|
||||
### 5. שימת לב להשפעות
|
||||
- על הועדה לשקול השפעות על הסביבה, שכנים, תשתיות
|
||||
|
||||
---
|
||||
|
||||
## טיפים למומחה הגישה השיפוטית
|
||||
|
||||
כשאתה מנתח החלטות בדיני תכנון ובניה, שים לב:
|
||||
|
||||
1. **לאיזה סעיפים בחוק מתייחסים** - אלו המקורות המשפטיים הבסיסיים
|
||||
2. **אילו פסקי דין מצוטטים** - אלו המקורות הפסיקתיים שהשופט מסתמך עליהם
|
||||
3. **איך מאזנים בין ערכים מנוגדים** - זכות קניין מול אינטרס ציבורי
|
||||
4. **רמת ההתערבות** - האם השופט נוטה להתערב או להותיר החלטות בעינן
|
||||
5. **יחס לחוות דעת מומחים** - האם סומך עליהן או ביקורתי
|
||||
6. **רמת הנמקה** - האם מפורט או תמציתי
|
||||
|
||||
זכור - זהו תחום משפטי טכני שדורש ידע מקצועי. השופטים בועדות ערר הם לרוב עורכי דין או אנשי מקצוע בתחום התכנון.
|
||||
635
skills/decision/scripts/create-decision-structure.cjs
Normal file
635
skills/decision/scripts/create-decision-structure.cjs
Normal file
@@ -0,0 +1,635 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* create-decision-structure.js — טיוטת מבנה החלטת ועדת ערר
|
||||
*
|
||||
* מייצר קובץ DOCX מעוצב עם כל חלקי ההחלטה (בלוקים א-יב).
|
||||
* בלוקים א-ט ממולאים בתוכן, בלוק י (דיון) ויא (סיכום) = placeholders.
|
||||
*
|
||||
* שימוש:
|
||||
* node create-decision-structure.js <input.json> [output.docx]
|
||||
*
|
||||
* מבוסס על create-legal-doc.js מתוך legal-docx skill.
|
||||
* כללי RTL: START/END (לא LEFT/RIGHT), bidi+bidirectional+rightToLeft בכל רמה.
|
||||
*
|
||||
* v1.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
Document, Packer, Paragraph, TextRun, Header, Footer,
|
||||
AlignmentType, HeadingLevel, PageNumber, LevelFormat,
|
||||
Table, TableRow, TableCell, WidthType, BorderStyle,
|
||||
ShadingType, UnderlineType
|
||||
} = require(path.join(process.cwd(), 'node_modules', 'docx'));
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CONFIGURATION
|
||||
// ═══════════════════════════════════════════════
|
||||
const FONT = "David";
|
||||
const FONT_SIZE = 24; // 12pt
|
||||
const HEADING1_SIZE = 32; // 16pt — "החלטה"
|
||||
const HEADING2_SIZE = 28; // 14pt — כותרות פרקים
|
||||
const MARGIN_CM = 2.5;
|
||||
const MARGIN_DXA = Math.round(MARGIN_CM / 2.54 * 1440); // 1417
|
||||
const PAGE_WIDTH = 11906; // A4
|
||||
const PAGE_HEIGHT = 16838;
|
||||
const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_DXA * 2; // 9072
|
||||
|
||||
const noBorders = {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// RTL HELPERS — מבוסס על create-legal-doc.js
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
const rtlRun = (text, opts = {}) => new TextRun({
|
||||
text,
|
||||
font: opts.font || FONT,
|
||||
size: opts.size || FONT_SIZE,
|
||||
bold: opts.bold || false,
|
||||
underline: opts.underline ? { type: UnderlineType.SINGLE } : undefined,
|
||||
rightToLeft: true,
|
||||
});
|
||||
|
||||
const rtlPara = (children, opts = {}) => new Paragraph({
|
||||
bidirectional: true,
|
||||
alignment: opts.alignment || AlignmentType.BOTH,
|
||||
spacing: opts.spacing || { after: 120, line: 276 }, // 1.15 line spacing = 276 twips
|
||||
indent: opts.indent,
|
||||
children: Array.isArray(children) ? children : [children],
|
||||
...(opts.heading ? { heading: opts.heading } : {}),
|
||||
});
|
||||
|
||||
// כותרת ראשית — "החלטה"
|
||||
const mainTitle = (text) => rtlPara(
|
||||
rtlRun(text, { bold: true, size: HEADING1_SIZE }),
|
||||
{ heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, spacing: { before: 240, after: 240 } }
|
||||
);
|
||||
|
||||
// כותרת פרק — "תמצית טענות הצדדים", "דיון והכרעה"
|
||||
const sectionTitle = (text) => rtlPara(
|
||||
rtlRun(text, { bold: true, size: HEADING2_SIZE, underline: true }),
|
||||
{ heading: HeadingLevel.HEADING_2, alignment: AlignmentType.CENTER, spacing: { before: 360, after: 240 } }
|
||||
);
|
||||
|
||||
// כותרת משנה — "טענות העוררים"
|
||||
const subTitle = (text) => rtlPara(
|
||||
rtlRun(text, { bold: true, size: FONT_SIZE }),
|
||||
{ alignment: AlignmentType.CENTER, spacing: { before: 240, after: 160 } }
|
||||
);
|
||||
|
||||
// סעיף ממוספר — מספר bold + טקסט רגיל
|
||||
const numberedPara = (num, text) => rtlPara([
|
||||
rtlRun(`${num}. `, { bold: true }),
|
||||
rtlRun(text),
|
||||
], { spacing: { after: 120, line: 276 } });
|
||||
|
||||
// ציטוט (blockquote) — הזחה משני הצדדים
|
||||
const blockquote = (text) => rtlPara(
|
||||
rtlRun(text),
|
||||
{ indent: { left: 567, right: 567 }, spacing: { before: 120, after: 120, line: 276 } }
|
||||
);
|
||||
|
||||
// תיבת תמונה — מסגרת אפורה עם הנחיה
|
||||
const imageBox = (description) => new Paragraph({
|
||||
bidirectional: true,
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 200, after: 200 },
|
||||
shading: { type: ShadingType.CLEAR, fill: "F0F0F0" },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `📷 תמונה: ${description}`,
|
||||
font: FONT,
|
||||
size: FONT_SIZE,
|
||||
rightToLeft: true,
|
||||
bold: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Placeholder — טקסט אפור שמסמן מקום
|
||||
const placeholder = (text) => rtlPara(
|
||||
new TextRun({
|
||||
text: `[${text}]`,
|
||||
font: FONT,
|
||||
size: FONT_SIZE,
|
||||
rightToLeft: true,
|
||||
italics: true,
|
||||
color: "808080",
|
||||
}),
|
||||
{ alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 } }
|
||||
);
|
||||
|
||||
// רווח
|
||||
const spacer = (after = 200) => rtlPara(rtlRun(""), { spacing: { after, before: 0 } });
|
||||
|
||||
// תא בטבלה
|
||||
const rtlCell = (children, width, opts = {}) => new TableCell({
|
||||
borders: noBorders,
|
||||
width: { size: width, type: WidthType.DXA },
|
||||
children: Array.isArray(children) ? children : [children],
|
||||
...(opts.verticalAlign ? { verticalAlign: opts.verticalAlign } : {}),
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// BLOCK BUILDERS — בוני הבלוקים
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
// בלוק א — כותרת מוסדית (טבלה 2 טורים)
|
||||
function buildInstitutionalHeader(data) {
|
||||
const leftCol = CONTENT_WIDTH * 0.5;
|
||||
const rightCol = CONTENT_WIDTH * 0.5;
|
||||
|
||||
// צד ימין — מוסד
|
||||
const rightCellContent = [
|
||||
rtlPara(rtlRun("מדינת ישראל", { bold: true }), { alignment: AlignmentType.START, spacing: { after: 0 } }),
|
||||
rtlPara(rtlRun("ועדת ערר לתכנון ובניה"), { alignment: AlignmentType.START, spacing: { after: 0 } }),
|
||||
rtlPara(rtlRun("מחוז ירושלים"), { alignment: AlignmentType.START, spacing: { after: 80 } }),
|
||||
];
|
||||
|
||||
// צד שמאל — מספרי תיק
|
||||
const leftLines = [];
|
||||
if (data.case_numbers) {
|
||||
data.case_numbers.forEach(cn => {
|
||||
leftLines.push(rtlPara([
|
||||
rtlRun("מס' תיק: "),
|
||||
rtlRun(cn, { bold: true }),
|
||||
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
|
||||
});
|
||||
}
|
||||
if (data.plan_number) {
|
||||
leftLines.push(rtlPara([
|
||||
rtlRun("מס' תכנית: "),
|
||||
rtlRun(data.plan_number, { bold: true }),
|
||||
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
|
||||
}
|
||||
if (data.request_number) {
|
||||
leftLines.push(rtlPara([
|
||||
rtlRun("מס' בקשה: "),
|
||||
rtlRun(data.request_number, { bold: true }),
|
||||
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
|
||||
}
|
||||
|
||||
return new Table({
|
||||
visuallyRightToLeft: true,
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [rightCol, leftCol],
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
rtlCell(rightCellContent, rightCol),
|
||||
rtlCell(leftLines.length ? leftLines : [rtlPara(rtlRun(""))], leftCol),
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// בלוק ב — הרכב הוועדה
|
||||
function buildPanel(data) {
|
||||
const lines = [];
|
||||
lines.push(spacer(120));
|
||||
lines.push(rtlPara([
|
||||
rtlRun("בפני:", { bold: true }),
|
||||
], { spacing: { after: 40 } }));
|
||||
lines.push(rtlPara([
|
||||
rtlRun("יו\"ר הוועדה: ", { bold: true }),
|
||||
rtlRun(data.panel?.chair || "עו\"ד דפנה תמיר"),
|
||||
], { spacing: { after: 40 } }));
|
||||
|
||||
if (data.panel?.members) {
|
||||
lines.push(rtlPara([
|
||||
rtlRun("חברי הוועדה: ", { bold: true }),
|
||||
rtlRun(data.panel.members[0] || ""),
|
||||
], { spacing: { after: 40 } }));
|
||||
for (let i = 1; i < data.panel.members.length; i++) {
|
||||
lines.push(rtlPara(rtlRun(data.panel.members[i]), { spacing: { after: 40 } }));
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// בלוק ג — צדדים
|
||||
function buildParties(data) {
|
||||
const lines = [];
|
||||
lines.push(spacer(120));
|
||||
|
||||
// עוררים
|
||||
if (data.appellants) {
|
||||
const label = data.appellants.length > 1 ? "העוררים:" : "העורר:";
|
||||
lines.push(rtlPara(rtlRun(label, { bold: true }), { spacing: { after: 40 } }));
|
||||
data.appellants.forEach(a => {
|
||||
lines.push(rtlPara(rtlRun(a.name), { spacing: { after: 20 } }));
|
||||
if (a.representative) {
|
||||
lines.push(rtlPara(rtlRun(`ע"י ב"כ ${a.representative}`), { spacing: { after: 20 } }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// נגד
|
||||
lines.push(spacer(80));
|
||||
lines.push(rtlPara(rtlRun("נגד", { bold: true }), {
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 80, after: 80 }
|
||||
}));
|
||||
|
||||
// משיבים
|
||||
if (data.respondents) {
|
||||
lines.push(rtlPara(rtlRun("המשיבים:", { bold: true }), { spacing: { after: 40 } }));
|
||||
data.respondents.forEach((r, i) => {
|
||||
lines.push(rtlPara(rtlRun(`${i + 1}. ${r.name}`), { spacing: { after: 20 } }));
|
||||
if (r.representative) {
|
||||
lines.push(rtlPara(rtlRun(`ע"י ב"כ ${r.representative}`), { spacing: { after: 20 } }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// בלוק ה — פתיחה
|
||||
function buildOpening(data) {
|
||||
const paras = [];
|
||||
if (data.opening_paragraphs) {
|
||||
data.opening_paragraphs.forEach((text, i) => {
|
||||
paras.push(numberedPara(i + 1, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(1, `לפנינו ${data.appeal_description || "[תיאור הערר]"}.`));
|
||||
}
|
||||
return paras;
|
||||
}
|
||||
|
||||
// בלוק ו — רקע עובדתי
|
||||
function buildBackground(data) {
|
||||
const paras = [];
|
||||
let num = (data.opening_paragraphs?.length || 1) + 1;
|
||||
|
||||
if (data.use_petach_davar !== false) {
|
||||
paras.push(sectionTitle("פתח דבר"));
|
||||
}
|
||||
|
||||
// מקרקעין
|
||||
if (data.property_description) {
|
||||
paras.push(numberedPara(num++, data.property_description));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[תיאור המקרקעין — מיקום, שטח, שכונה, ייעוד, מאפיינים ייחודיים]"));
|
||||
}
|
||||
|
||||
// היסטוריה תכנונית
|
||||
if (data.planning_history) {
|
||||
data.planning_history.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[היסטוריה תכנונית — תכניות קודמות, החלטות קודמות, היתרים]"));
|
||||
}
|
||||
|
||||
// תמונה — מיקום
|
||||
paras.push(imageBox("תשריט מיקום המגרש מתוך מערכת GIS — לסמן את המגרש"));
|
||||
|
||||
// מהות הבקשה
|
||||
if (data.request_essence) {
|
||||
if (Array.isArray(data.request_essence)) {
|
||||
data.request_essence.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, data.request_essence));
|
||||
}
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[מהות הבקשה — פירוט מלא של מה שהתבקש]"));
|
||||
}
|
||||
|
||||
// תמונה — תשריט
|
||||
paras.push(imageBox("תשריט הבקשה / נספח בינוי / תכנית מוצעת"));
|
||||
|
||||
// ציטוט מפרוטוקול
|
||||
if (data.committee_protocol_quote) {
|
||||
paras.push(numberedPara(num++, "להלן מתוך פרוטוקול הדיון בוועדה המקומית:"));
|
||||
paras.push(blockquote(data.committee_protocol_quote));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[ציטוט מלא מפרוטוקול הוועדה המקומית]"));
|
||||
}
|
||||
|
||||
// החלטת הוועדה + תנאים
|
||||
if (data.committee_decision) {
|
||||
paras.push(numberedPara(num++, data.committee_decision));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[החלטת הוועדה המקומית — מה הוחלט, אילו תנאים נקבעו]"));
|
||||
}
|
||||
|
||||
// תמונה אופציונלית — סביבה
|
||||
if (data.include_aerial_photo !== false) {
|
||||
paras.push(imageBox("צילום אוויר / מבט על הסביבה עם סימון המגרש"));
|
||||
}
|
||||
|
||||
// סביבת המקרקעין
|
||||
if (data.surroundings) {
|
||||
paras.push(numberedPara(num++, data.surroundings));
|
||||
}
|
||||
|
||||
// הגשת הערר
|
||||
if (data.appeal_filing) {
|
||||
paras.push(numberedPara(num++, data.appeal_filing));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[הגשת הערר — תאריך, מי הגיש, הצטרפויות]"));
|
||||
}
|
||||
|
||||
return { paras, nextNum: num };
|
||||
}
|
||||
|
||||
// בלוק ז — טענות הצדדים
|
||||
function buildClaims(data, startNum) {
|
||||
const paras = [];
|
||||
let num = startNum;
|
||||
|
||||
paras.push(sectionTitle("תמצית טענות הצדדים"));
|
||||
|
||||
// טענות העוררים
|
||||
if (data.appellant_claims_sections) {
|
||||
// מספר עוררים עם כותרות נפרדות
|
||||
data.appellant_claims_sections.forEach(section => {
|
||||
paras.push(subTitle(section.title));
|
||||
section.claims.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
paras.push(subTitle("טענות העוררים"));
|
||||
if (data.appellant_claims) {
|
||||
data.appellant_claims.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[טענות העוררים]"));
|
||||
}
|
||||
}
|
||||
|
||||
// עמדת הוועדה המקומית
|
||||
paras.push(subTitle("עמדת הוועדה המקומית"));
|
||||
if (data.committee_position) {
|
||||
data.committee_position.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[עמדת הוועדה המקומית]"));
|
||||
}
|
||||
|
||||
// עמדת מבקשי ההיתר / מגישי התכנית
|
||||
const applicantTitle = data.type === "plan"
|
||||
? "עמדת מגישי התכנית"
|
||||
: "עמדת מבקשי ההיתר";
|
||||
paras.push(subTitle(applicantTitle));
|
||||
if (data.applicant_position) {
|
||||
data.applicant_position.forEach(text => {
|
||||
paras.push(numberedPara(num++, text));
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, `[${applicantTitle}]`));
|
||||
}
|
||||
|
||||
return { paras, nextNum: num };
|
||||
}
|
||||
|
||||
// בלוק ח — ההליכים בפני ועדת הערר
|
||||
function buildProceedings(data, startNum) {
|
||||
const paras = [];
|
||||
let num = startNum;
|
||||
|
||||
paras.push(sectionTitle("ההליכים בפני ועדת הערר"));
|
||||
|
||||
if (data.proceedings) {
|
||||
data.proceedings.forEach(item => {
|
||||
if (item.type === "image") {
|
||||
paras.push(imageBox(item.description));
|
||||
} else if (item.type === "quote") {
|
||||
paras.push(numberedPara(num++, item.intro || ""));
|
||||
paras.push(blockquote(item.text));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, item.text));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[דיון — תאריך, נוכחים, עיקרי הדברים]"));
|
||||
paras.push(numberedPara(num++, "[סיור (אם היה) — תאריך, תיאור]"));
|
||||
paras.push(imageBox("צילומים מהסיור"));
|
||||
paras.push(numberedPara(num++, "[החלטות ביניים]"));
|
||||
paras.push(numberedPara(num++, "[השלמות טיעון — כרונולוגי]"));
|
||||
paras.push(numberedPara(num++, "[חוו\"ד מקצועיות שהתקבלו]"));
|
||||
paras.push(imageBox("הדמיות / חתכי בינוי מהשלמות טיעון (אם יש)"));
|
||||
}
|
||||
|
||||
return { paras, nextNum: num };
|
||||
}
|
||||
|
||||
// בלוק ט — תכניות חלות (אופציונלי)
|
||||
function buildPlans(data, startNum) {
|
||||
if (data.skip_plans_section) return { paras: [], nextNum: startNum };
|
||||
|
||||
const paras = [];
|
||||
let num = startNum;
|
||||
|
||||
paras.push(sectionTitle("התכניות החלות על המקרקעין"));
|
||||
|
||||
if (data.applicable_plans) {
|
||||
data.applicable_plans.forEach(item => {
|
||||
if (item.type === "quote") {
|
||||
paras.push(numberedPara(num++, item.intro || ""));
|
||||
paras.push(blockquote(item.text));
|
||||
} else {
|
||||
paras.push(numberedPara(num++, item.text));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
paras.push(numberedPara(num++, "[פירוט התכניות הרלוונטיות עם ציטוט מהוראותיהן]"));
|
||||
}
|
||||
|
||||
return { paras, nextNum: num };
|
||||
}
|
||||
|
||||
// בלוק יב — חתימות
|
||||
function buildSignatures(data) {
|
||||
const chairName = data.panel?.chair || "עו\"ד דפנה תמיר";
|
||||
const secretaryName = data.secretary || "";
|
||||
|
||||
const halfWidth = Math.floor(CONTENT_WIDTH / 2);
|
||||
|
||||
return [
|
||||
spacer(400),
|
||||
rtlPara(rtlRun("ניתנה פה אחד, היום ______________, ______________."), {
|
||||
spacing: { after: 400 }
|
||||
}),
|
||||
new Table({
|
||||
visuallyRightToLeft: true,
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [halfWidth, halfWidth],
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
rtlCell([
|
||||
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }),
|
||||
rtlPara(rtlRun(chairName, { bold: true }), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
|
||||
rtlPara(rtlRun("יו\"ר ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
|
||||
], halfWidth),
|
||||
rtlCell([
|
||||
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }),
|
||||
rtlPara(rtlRun(secretaryName || ""), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
|
||||
rtlPara(rtlRun("מזכירת ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
|
||||
], halfWidth),
|
||||
]
|
||||
})
|
||||
]
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// MAIN — הרכבת המסמך
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
function buildDocument(data) {
|
||||
const content = [];
|
||||
|
||||
// בלוק א — כותרת מוסדית
|
||||
content.push(buildInstitutionalHeader(data));
|
||||
content.push(spacer(160));
|
||||
|
||||
// בלוק ב — הרכב
|
||||
content.push(...buildPanel(data));
|
||||
content.push(spacer(80));
|
||||
|
||||
// בלוק ג — צדדים
|
||||
content.push(...buildParties(data));
|
||||
content.push(spacer(160));
|
||||
|
||||
// בלוק ד — כותרת "החלטה"
|
||||
content.push(mainTitle("החלטה"));
|
||||
|
||||
// בלוק ה — פתיחה
|
||||
content.push(...buildOpening(data));
|
||||
|
||||
// בלוק ו — רקע
|
||||
const bg = buildBackground(data);
|
||||
content.push(...bg.paras);
|
||||
|
||||
// בלוק ז — טענות
|
||||
const claims = buildClaims(data, bg.nextNum);
|
||||
content.push(...claims.paras);
|
||||
|
||||
// בלוק ח — הליכים
|
||||
const proc = buildProceedings(data, claims.nextNum);
|
||||
content.push(...proc.paras);
|
||||
|
||||
// בלוק ט — תכניות (אופציונלי)
|
||||
const plans = buildPlans(data, proc.nextNum);
|
||||
content.push(...plans.paras);
|
||||
|
||||
// בלוק י — דיון והכרעה (placeholder)
|
||||
content.push(sectionTitle("דיון והכרעה"));
|
||||
content.push(placeholder("כאן מתחיל פרק הדיון וההכרעה — ייכתב בשלב הבא"));
|
||||
|
||||
// בלוק יא — סיכום (placeholder)
|
||||
content.push(sectionTitle("סיכום"));
|
||||
content.push(placeholder("ייכתב לאחר השלמת פרק הדיון"));
|
||||
|
||||
// בלוק יב — חתימות
|
||||
content.push(...buildSignatures(data));
|
||||
|
||||
return new Document({
|
||||
styles: {
|
||||
default: {
|
||||
document: {
|
||||
run: { font: FONT, size: FONT_SIZE, rightToLeft: true },
|
||||
paragraph: { bidirectional: true, alignment: AlignmentType.BOTH }
|
||||
}
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal",
|
||||
quickFormat: true,
|
||||
run: { size: HEADING1_SIZE, bold: true, font: FONT, rightToLeft: true },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0,
|
||||
bidirectional: true, alignment: AlignmentType.CENTER }
|
||||
},
|
||||
{
|
||||
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal",
|
||||
quickFormat: true,
|
||||
run: { size: HEADING2_SIZE, bold: true, font: FONT, rightToLeft: true },
|
||||
paragraph: { spacing: { before: 200, after: 200 }, outlineLevel: 1,
|
||||
bidirectional: true, alignment: AlignmentType.CENTER }
|
||||
},
|
||||
]
|
||||
},
|
||||
sections: [{
|
||||
properties: {
|
||||
page: {
|
||||
size: { width: PAGE_WIDTH, height: PAGE_HEIGHT },
|
||||
margin: { top: MARGIN_DXA, right: MARGIN_DXA, bottom: MARGIN_DXA, left: MARGIN_DXA }
|
||||
},
|
||||
bidi: true,
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [new Paragraph({
|
||||
bidirectional: true,
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
rtlRun("עמוד ", { size: 18 }),
|
||||
new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18 }),
|
||||
rtlRun(" מתוך ", { size: 18 }),
|
||||
new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18 }),
|
||||
]
|
||||
})]
|
||||
})
|
||||
},
|
||||
children: content
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CLI
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
async function main() {
|
||||
const inputFile = process.argv[2];
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('שימוש: node create-decision-structure.js <input.json> [output.docx]');
|
||||
console.error('');
|
||||
console.error('קובץ ה-JSON צריך לכלול:');
|
||||
console.error(' case_numbers, panel, appellants, respondents,');
|
||||
console.error(' opening_paragraphs, property_description, planning_history,');
|
||||
console.error(' request_essence, committee_protocol_quote, committee_decision,');
|
||||
console.error(' appellant_claims, committee_position, applicant_position,');
|
||||
console.error(' proceedings, applicable_plans');
|
||||
console.error('');
|
||||
console.error('ראה: .claude/skills/legal-decision/references/decision-template.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
|
||||
const outputFile = process.argv[3] || `החלטה-ערר-${(data.case_numbers?.[0] || 'draft').replace(/\//g, '-')}-מבנה.docx`;
|
||||
|
||||
const doc = buildDocument(data);
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
fs.writeFileSync(outputFile, buffer);
|
||||
|
||||
console.log(`✅ ${outputFile}`);
|
||||
console.log(` פונט: ${FONT} ${FONT_SIZE / 2}pt`);
|
||||
console.log(` שוליים: ${MARGIN_CM} ס"מ`);
|
||||
console.log(` RTL: bidi + bidirectional + rightToLeft ✓`);
|
||||
console.log(` Alignment: START/END ✓`);
|
||||
console.log(` גודל: ${(buffer.length / 1024).toFixed(1)} KB`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ שגיאה:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
204
skills/decision/scripts/package-lock.json
generated
Normal file
204
skills/decision/scripts/package-lock.json
generated
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"docx": "^9.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.6.1.tgz",
|
||||
"integrity": "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
|
||||
"integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
skills/decision/scripts/package.json
Normal file
5
skills/decision/scripts/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"docx": "^9.6.1"
|
||||
}
|
||||
}
|
||||
976
skills/docx/SKILL.md
Normal file
976
skills/docx/SKILL.md
Normal file
@@ -0,0 +1,976 @@
|
||||
---
|
||||
name: legal-docx
|
||||
description: >
|
||||
יצירת מסמכים משפטיים בעברית בפורמט DOCX עם תמיכה מלאה ב-RTL, עקוב אחר שינויים,
|
||||
והערות. משתמש בסקיל הבסיסי docx ומוסיף התמחות בתחום המשפטי הישראלי.
|
||||
|
||||
טריגרים: "מסמך משפטי", "הסכם", "כתב הגנה", "כתב תביעה", "בקשה", "תצהיר",
|
||||
"מכתב התראה", "חוזה", "הסכם שירותים", "ייפוי כוח", "פרוטוקול", "החלטה",
|
||||
"צו", "פסק דין", "כתב טענות", בקשה ליצור מסמך DOCX בעברית, מסמך RTL,
|
||||
"tracked changes בעברית", "הערות שוליים משפטיות", "עקוב אחר שינויים".
|
||||
|
||||
גם מתאים כאשר המשתמש מבקש מסמך Word בעברית עם פונט David/FrankRuehl/Miriam,
|
||||
שוליים 2.5 ס"מ, או כל מסמך מקצועי בעברית שדורש עיצוב משפטי מדויק.
|
||||
|
||||
פיצ'רים: טבלאות RTL, הערות שוליים, תוכן עניינים, היפרלינקים,
|
||||
לוגו/נייר פירמה, עריכת DOCX קיים, tracked changes, comments,
|
||||
מרווח שורות, קו תחתי, מספר סקשנים, זיהוי אוטומטי של סוג מסמך.
|
||||
---
|
||||
|
||||
# Legal DOCX v3.0 — מסמכים משפטיים בעברית
|
||||
|
||||
סקיל זה מרחיב את סקיל docx הבסיסי עם התמחות במסמכים משפטיים ישראליים.
|
||||
|
||||
**תמיד לקרוא קודם** את `/mnt/skills/public/docx/SKILL.md` — הסקיל הזה מניח שאתה מכיר את תהליך העבודה הבסיסי (docx-js, unpack/pack, tracked changes, comments).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 קריטי: כללי RTL שחייבים לזכור
|
||||
|
||||
### הכלל המרכזי: START/END במקום LEFT/RIGHT
|
||||
|
||||
**במסמך עברי עם `bidirectional: true`, לעולם אל תשתמש ב-`AlignmentType.LEFT` או `AlignmentType.RIGHT` לפסקאות ומספור!**
|
||||
|
||||
| רוצה יישור ל... | ❌ לא להשתמש | ✅ להשתמש |
|
||||
|-----------------|-------------|----------|
|
||||
| **ימין** | `LEFT` או `RIGHT` | `AlignmentType.START` |
|
||||
| **שמאל** | `LEFT` או `RIGHT` | `AlignmentType.END` |
|
||||
| **מרכז** | — | `AlignmentType.CENTER` |
|
||||
| **שני צדדים** | — | `AlignmentType.BOTH` |
|
||||
|
||||
> **למה?** כש-`bidirectional: true`, Word מתבלבל עם LEFT/RIGHT. `START` = התחלה = ימין ב-RTL, `END` = סוף = שמאל ב-RTL.
|
||||
|
||||
### שלוש הגדרות RTL חובה
|
||||
|
||||
כל מסמך עברי חייב את **שלושת** ההגדרות הבאות בכל הרמות:
|
||||
|
||||
```javascript
|
||||
// 1. ברמת ה-Section
|
||||
sections: [{
|
||||
properties: {
|
||||
bidi: true // ← חובה!
|
||||
}
|
||||
}]
|
||||
|
||||
// 2. ברמת כל Paragraph
|
||||
new Paragraph({
|
||||
bidirectional: true, // ← חובה!
|
||||
alignment: AlignmentType.BOTH, // או START/CENTER/END
|
||||
})
|
||||
|
||||
// 3. ברמת כל TextRun
|
||||
new TextRun({
|
||||
text: "טקסט בעברית",
|
||||
rightToLeft: true, // ← חובה!
|
||||
font: "David",
|
||||
})
|
||||
```
|
||||
|
||||
**חוסר באחת מהן = יישור שגוי או טקסט הפוך!**
|
||||
|
||||
---
|
||||
|
||||
## זיהוי סוג מסמך — Document Type Detection
|
||||
|
||||
**לפני יצירת מסמך, זהה את סוגו.** לכל סוג יש מבנה שונה:
|
||||
|
||||
| סוג מסמך | דוגמאות | Header בית משפט? | מבנה מיוחד |
|
||||
|----------|---------|------------------|------------|
|
||||
| **כתב טענות** | תביעה, הגנה, בקשה, ערעור, תצהיר, בר"ע | ✅ כן | טבלת Header עם בית משפט + מספר תיק |
|
||||
| **מכתב התראה** | התראה, דרישה, מכתב עו"ד | ❌ לא | לוגו/פרטי משרד, "הנדון:", חתימה |
|
||||
| **הסכם/חוזה** | הסכם שירותים, NDA, חוזה שכירות | ❌ לא | הואילים, צדדים, חתימות בשני טורים |
|
||||
| **מסמך כללי** | חוות דעת, מזכר, סיכום | ❌ לא | לפי הצורך |
|
||||
|
||||
### טריגרים לזיהוי
|
||||
|
||||
```
|
||||
כתב טענות ← "בית משפט", "בית הדין", "תביעה", "הגנה", "בקשה",
|
||||
"ערעור", "תצהיר", "המבקש", "המשיב", "התובע", "הנתבע", "בר\"ע"
|
||||
|
||||
מכתב התראה ← "התראה", "דרישה", "לכבוד", "הנדון:", "נשלח מבלי לפגוע"
|
||||
|
||||
הסכם/חוזה ← "הסכם", "חוזה", "בין:", "לבין:", "הואיל", "צד א'", "צד ב'",
|
||||
"ולראיה באו הצדדים"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## פונטים ומידות
|
||||
|
||||
### פונטים משפטיים
|
||||
|
||||
| פונט | שימוש | size (half-points) |
|
||||
|------|-------|-------------------|
|
||||
| **David** | ברירת מחדל, גוף טקסט | 24 (12pt) |
|
||||
| **FrankRuehl** | פורמלי/שמרני | 24 (12pt) |
|
||||
| **Miriam** | מודרני יותר | 24 (12pt) |
|
||||
|
||||
**חשוב:** תמיד להגדיר גם `w:cs` (Complex Script) וגם `w:ascii`/`w:hAnsi`:
|
||||
```javascript
|
||||
new TextRun({ text: "...", font: "David", rightToLeft: true })
|
||||
// docx-js מייצר: <w:rFonts w:ascii="David" w:cs="David" w:eastAsia="David" w:hAnsi="David"/>
|
||||
```
|
||||
|
||||
### מידות ושוליים
|
||||
|
||||
```
|
||||
2.5 ס"מ = 1417 DXA (ברירת מחדל משפטי)
|
||||
3.0 ס"מ = 1701 DXA
|
||||
2.0 ס"מ = 1134 DXA
|
||||
1.0 אינץ' = 1440 DXA
|
||||
|
||||
A4 = 11906 × 16838 DXA
|
||||
רוחב תוכן (A4, שוליים 2.5 ס"מ) = 9072 DXA
|
||||
רוחב תוכן (A4, שוליים 2.0 ס"מ) = 9638 DXA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## מספור סעיפים משפטיים
|
||||
|
||||
**⚠️ שים לב: `alignment: AlignmentType.START` — לא LEFT ולא RIGHT!**
|
||||
|
||||
```javascript
|
||||
numbering: {
|
||||
config: [{
|
||||
reference: "legal-clauses",
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: "%1.",
|
||||
alignment: AlignmentType.START, // ✅ START — לא RIGHT!
|
||||
suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } }
|
||||
},
|
||||
{
|
||||
level: 1,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: "%1.%2",
|
||||
alignment: AlignmentType.START, // ✅ START — לא RIGHT!
|
||||
suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 1440, hanging: 500 } } }
|
||||
},
|
||||
{
|
||||
level: 2,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: "%1.%2.%3",
|
||||
alignment: AlignmentType.START, // ✅ START
|
||||
suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 2160, hanging: 640 } } }
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**שימוש:**
|
||||
```javascript
|
||||
new Paragraph({
|
||||
bidirectional: true,
|
||||
numbering: { reference: "legal-clauses", level: 0 },
|
||||
children: [new TextRun({ text: "תוכן הסעיף", font: "David", size: 24, rightToLeft: true })]
|
||||
})
|
||||
```
|
||||
|
||||
> **הבעיה שנפתרה:** בלי `AlignmentType.START`, המספור מופיע כ-".1" במקום "1."
|
||||
|
||||
---
|
||||
|
||||
## טבלאות RTL
|
||||
|
||||
**⚠️ קריטי: `visuallyRightToLeft: true` — בלי זה העמודות יהיו הפוכות!**
|
||||
|
||||
```javascript
|
||||
const { Table, TableRow, TableCell, BorderStyle, WidthType, ShadingType } = require('docx');
|
||||
|
||||
const CONTENT_WIDTH = 9072; // A4 עם שוליים 2.5 ס"מ (11906 - 1417×2)
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: "999999" };
|
||||
const borders = { top: border, bottom: border, left: border, right: border };
|
||||
const noBorders = {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }
|
||||
};
|
||||
|
||||
// Helper function לתאים בעברית
|
||||
const rtlCell = (text, width, opts = {}) => new TableCell({
|
||||
borders: opts.noBorders ? noBorders : borders,
|
||||
width: { size: width, type: WidthType.DXA },
|
||||
margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
||||
...(opts.shading ? { shading: { fill: opts.shading, type: ShadingType.CLEAR } } : {}),
|
||||
children: [new Paragraph({
|
||||
bidirectional: true,
|
||||
alignment: opts.alignment || AlignmentType.CENTER,
|
||||
children: [new TextRun({
|
||||
text, font: "David", size: 24, rightToLeft: true, bold: opts.bold
|
||||
})]
|
||||
})]
|
||||
});
|
||||
|
||||
// טבלה עם גבולות
|
||||
new Table({
|
||||
visuallyRightToLeft: true, // ✅ קריטי! בלי זה העמודות הפוכות
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [4536, 2268, 2268], // חייב להסתכם ל-CONTENT_WIDTH
|
||||
rows: [
|
||||
new TableRow({ children: [
|
||||
rtlCell("סוג שירות", 4536, { bold: true, shading: "D5E8F0" }),
|
||||
rtlCell("תעריף", 2268, { bold: true, shading: "D5E8F0" }),
|
||||
rtlCell("הערות", 2268, { bold: true, shading: "D5E8F0" }),
|
||||
]}),
|
||||
new TableRow({ children: [
|
||||
rtlCell("ייעוץ משפטי", 4536),
|
||||
rtlCell("850 ש״ח", 2268),
|
||||
rtlCell("בתוספת מע״מ", 2268),
|
||||
]}),
|
||||
]
|
||||
})
|
||||
|
||||
// טבלה ללא גבולות (לחתימות / header)
|
||||
new Table({
|
||||
visuallyRightToLeft: true,
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
|
||||
rows: [
|
||||
new TableRow({ children: [
|
||||
rtlCell("חתימה: ________", CONTENT_WIDTH / 2, { noBorders: true, alignment: AlignmentType.CENTER }),
|
||||
rtlCell("חתימה: ________", CONTENT_WIDTH / 2, { noBorders: true, alignment: AlignmentType.CENTER }),
|
||||
]})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**כללים:**
|
||||
- **`visuallyRightToLeft: true`** — חובה! בלי זה העמודות משמאל לימין
|
||||
- **`WidthType.DXA`** — לא PERCENTAGE (פחות אמין ב-RTL)
|
||||
- **`columnWidths`** — סכום חייב להיות שווה ל-`CONTENT_WIDTH`
|
||||
- **`bidirectional: true` + `rightToLeft: true`** — בכל תא
|
||||
|
||||
---
|
||||
|
||||
## Tracked Changes — עקוב אחר שינויים
|
||||
|
||||
### שם מחבר בעברית
|
||||
```xml
|
||||
<w:del w:id="10" w:author="עו"ד כהן" w:date="2026-02-06T09:00:00Z">
|
||||
```
|
||||
|
||||
### שינוי ערך (סכום, תאריך, תקופה)
|
||||
פצל את הטקסט ועטוף רק את הערך שמשתנה:
|
||||
```xml
|
||||
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||
<w:t xml:space="preserve">שכר הטרחה יעמוד על סך של </w:t></w:r>
|
||||
<w:del w:id="10" w:author="עו"ד כהן" w:date="...">
|
||||
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:delText>750</w:delText></w:r>
|
||||
</w:del>
|
||||
<w:ins w:id="11" w:author="עו"ד כהן" w:date="...">
|
||||
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:t>850</w:t></w:r>
|
||||
</w:ins>
|
||||
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||
<w:t xml:space="preserve"> ש״ח לשעת עבודה</w:t></w:r>
|
||||
```
|
||||
|
||||
### מחיקת סעיף שלם
|
||||
סמן גם את ה-paragraph mark כ-deleted:
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:bidi/>
|
||||
<w:jc w:val="both"/>
|
||||
<w:rPr>
|
||||
<w:del w:id="20" w:author="עו"ד כהן" w:date="..."/>
|
||||
</w:rPr>
|
||||
</w:pPr>
|
||||
<w:del w:id="21" w:author="עו"ד כהן" w:date="...">
|
||||
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||
<w:delText>הסעיף שנמחק</w:delText></w:r>
|
||||
</w:del>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### RTL PROPS — בלוק rPr מלא לכל run
|
||||
```xml
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="David" w:cs="David" w:eastAsia="David" w:hAnsi="David"/>
|
||||
<w:sz w:val="24"/>
|
||||
<w:szCs w:val="24"/>
|
||||
<w:rtl/>
|
||||
</w:rPr>
|
||||
```
|
||||
|
||||
### קבלה/דחייה של שינויים
|
||||
|
||||
**קבלת Insertion:**
|
||||
```
|
||||
לפני: <w:ins w:id="5" w:author="..."><w:r>...<w:t>טקסט חדש</w:t></w:r></w:ins>
|
||||
אחרי: <w:r>...<w:t>טקסט חדש</w:t></w:r>
|
||||
→ הסר את תגית <w:ins> ושמור את התוכן הפנימי.
|
||||
```
|
||||
|
||||
**דחיית Insertion:**
|
||||
```
|
||||
לפני: <w:ins w:id="5" w:author="..."><w:r>...<w:t>טקסט חדש</w:t></w:r></w:ins>
|
||||
אחרי: (הסר לחלוטין)
|
||||
→ מחק את כל בלוק ה-<w:ins> כולל תוכנו.
|
||||
```
|
||||
|
||||
**קבלת מחיקה:**
|
||||
```
|
||||
לפני: <w:del w:id="10" w:author="..."><w:r>...<w:delText>טקסט שנמחק</w:delText></w:r></w:del>
|
||||
אחרי: (הסר לחלוטין)
|
||||
→ מחק את כל בלוק ה-<w:del> כולל תוכנו — המחיקה מתקבלת.
|
||||
```
|
||||
|
||||
**שחזור טקסט מקורי (דחיית מחיקה):**
|
||||
```
|
||||
לפני: <w:del w:id="10" w:author="..."><w:r>...<w:delText>טקסט מקורי</w:delText></w:r></w:del>
|
||||
אחרי: <w:r>...<w:t>טקסט מקורי</w:t></w:r>
|
||||
→ הסר <w:del>, החלף <w:delText> ב-<w:t>, הסר <w:del> מ-rPr אם קיים.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## הערות (Comments)
|
||||
|
||||
הערות משמשות בסקירה משפטית להסביר *למה* בוצע שינוי:
|
||||
|
||||
```bash
|
||||
python /mnt/skills/public/docx/scripts/comment.py unpacked/ 0 "הערה בעברית" --author "עו״ד כהן"
|
||||
```
|
||||
|
||||
שימושים נפוצים:
|
||||
- הסבר לשינוי סכום או תאריך
|
||||
- דגל על סעיף בעייתי
|
||||
- הפניה לפסיקה או חקיקה
|
||||
- שאלה ללקוח / לצד השני
|
||||
|
||||
> **הערה:** `comment.py` מטפל אוטומטית ב-Content_Types ו-relationships.
|
||||
|
||||
---
|
||||
|
||||
## עריכת DOCX קיים (Unpack → Edit → Pack)
|
||||
|
||||
### תהליך מאומת
|
||||
```bash
|
||||
# 1. פתיחת הקובץ
|
||||
python /mnt/skills/public/docx/scripts/unpack.py input.docx unpacked/
|
||||
|
||||
# 2. עריכת word/document.xml (או קבצי XML אחרים)
|
||||
|
||||
# 3. ארגון מחדש
|
||||
python /mnt/skills/public/docx/scripts/pack.py unpacked/ output.docx --original input.docx
|
||||
```
|
||||
|
||||
### מיקום הוספת תוכן — כלל קריטי
|
||||
```
|
||||
⚠️ פסקאות חדשות חייבות להיכנס *לפני* <w:sectPr> האחרון בגוף המסמך.
|
||||
הוספה *אחרי* sectPr תיכשל בוולידציה.
|
||||
|
||||
מבנה תקין:
|
||||
<w:body>
|
||||
<w:p>...</w:p> ← פסקאות קיימות
|
||||
<w:p>...</w:p> ← פסקה חדשה כאן ✅
|
||||
<w:sectPr>...</w:sectPr> ← תמיד אחרון
|
||||
</w:body>
|
||||
```
|
||||
|
||||
### דוגמה — הוספת פסקה בעברית
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:bidi/>
|
||||
<w:jc w:val="both"/>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="David" w:cs="David" w:eastAsia="David" w:hAnsi="David"/>
|
||||
<w:sz w:val="24"/>
|
||||
<w:szCs w:val="24"/>
|
||||
<w:rtl/>
|
||||
</w:rPr>
|
||||
<w:t>הטקסט החדש</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## הערות שוליים (Footnotes)
|
||||
|
||||
**השימוש המרכזי:** הפניות לחקיקה ופסיקה.
|
||||
|
||||
```javascript
|
||||
const { FootnoteReferenceRun } = require('docx');
|
||||
|
||||
// 1. הגדרה ב-Document:
|
||||
const doc = new Document({
|
||||
footnotes: {
|
||||
1: { children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START, // ✅ START
|
||||
children: [new TextRun({
|
||||
text: "חוק החוזים (חלק כללי), התשל״ג-1973, סעיף 12.",
|
||||
font: "David", size: 20, rightToLeft: true // 10pt להערות שוליים
|
||||
})]
|
||||
})] },
|
||||
2: { children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
children: [new TextRun({
|
||||
text: "ע״א 1234/20 כהן נ׳ לוי, פסקה 15 (פורסם בנבו, 1.1.2024).",
|
||||
font: "David", size: 20, rightToLeft: true
|
||||
})]
|
||||
})] },
|
||||
},
|
||||
// ...sections
|
||||
});
|
||||
|
||||
// 2. הפניה בגוף הטקסט:
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.BOTH,
|
||||
children: [
|
||||
new TextRun({ text: "חובת תום הלב", font: "David", size: 24, rightToLeft: true }),
|
||||
new FootnoteReferenceRun(1),
|
||||
new TextRun({ text: " חלה על כל שלבי המשא ומתן", font: "David", size: 24, rightToLeft: true }),
|
||||
new FootnoteReferenceRun(2),
|
||||
new TextRun({ text: ".", font: "David", size: 24, rightToLeft: true }),
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### תיקון RTL בהערות שוליים (post-unpack)
|
||||
docx-js לא מגדיר RTL מלא בהערות שוליים. אחרי unpack, צריך לתקן ב-`word/footnotes.xml`:
|
||||
```xml
|
||||
<!-- 1. הוסף pStyle + bidi לכל הערת שוליים: -->
|
||||
<w:footnote w:id="1">
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="FootnoteText"/>
|
||||
<w:bidi/>
|
||||
<w:jc w:val="start"/>
|
||||
</w:pPr>
|
||||
...
|
||||
|
||||
<!-- 2. הוסף rtl ל-footnoteRef run: -->
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rStyle w:val="FootnoteReference"/>
|
||||
<w:rtl/>
|
||||
</w:rPr>
|
||||
<w:footnoteRef/>
|
||||
</w:r>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## מרווח שורות (Line Spacing)
|
||||
|
||||
**דרישת בתי המשפט:** בדרך כלל 1.5 שורות.
|
||||
|
||||
```javascript
|
||||
const { LineRuleType } = require('docx');
|
||||
|
||||
// LineRuleType.AUTO — הערך הוא ב-1/240 שורה
|
||||
spacing: { line: 240, lineRule: LineRuleType.AUTO } // 1.0 — צפוף
|
||||
spacing: { line: 276, lineRule: LineRuleType.AUTO } // 1.15 — ברירת מחדל Word
|
||||
spacing: { line: 360, lineRule: LineRuleType.AUTO } // 1.5 — נדרש בבתי משפט
|
||||
spacing: { line: 480, lineRule: LineRuleType.AUTO } // 2.0 — כפול
|
||||
|
||||
// שילוב עם before/after:
|
||||
spacing: { line: 360, lineRule: LineRuleType.AUTO, before: 120, after: 120 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## תוכן עניינים (TOC)
|
||||
|
||||
**⚠️ חובה: TOC ידני (לא TableOfContents).**
|
||||
`TableOfContents` של docx-js מייצר שדה שוורד מעדכן ב-F9 ומאבד הגדרות RTL.
|
||||
|
||||
```javascript
|
||||
const { Tab, TabStopType, LeaderType, PageBreak } = require('docx');
|
||||
|
||||
// שורת TOC ידנית
|
||||
const tocEntry = (text, pageNum, opts = {}) => new Paragraph({
|
||||
bidirectional: true,
|
||||
spacing: { after: 60, line: 276, lineRule: LineRuleType.AUTO },
|
||||
...(opts.indent ? { indent: { right: opts.indent } } : {}),
|
||||
tabStops: [{ type: TabStopType.RIGHT, position: 9026, leader: LeaderType.DOT }],
|
||||
children: [
|
||||
new TextRun({
|
||||
text, font: "David", size: 24, rightToLeft: true,
|
||||
bold: opts.bold || false,
|
||||
}),
|
||||
new TextRun({ children: [new Tab()], font: "David", rightToLeft: true }),
|
||||
new TextRun({
|
||||
text: String(pageNum), font: "David", size: 24, rightToLeft: true,
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
// שימוש:
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 200 },
|
||||
children: [new TextRun({
|
||||
text: "תוכן עניינים", font: "David", size: 32, bold: true, rightToLeft: true
|
||||
})]
|
||||
}),
|
||||
tocEntry("פרק א׳ — הגדרות כלליות", 2, { bold: true }),
|
||||
tocEntry("1. הגדרות יסוד", 2, { indent: 400 }),
|
||||
tocEntry("פרק ב׳ — השירותים", 3, { bold: true }),
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## קו תחתי (Underline)
|
||||
|
||||
```javascript
|
||||
const { UnderlineType } = require('docx');
|
||||
|
||||
// קו תחתי רגיל:
|
||||
new TextRun({
|
||||
text: "נושא: הסכם שירותים",
|
||||
font: "David", size: 24, rightToLeft: true,
|
||||
underline: { type: UnderlineType.SINGLE }
|
||||
})
|
||||
|
||||
// קו תחתי כפול (לכותרות חשובות):
|
||||
underline: { type: UnderlineType.DOUBLE }
|
||||
|
||||
// סוגים שימושיים: SINGLE, DOUBLE, THICK, DOTTED, DASH, WAVE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## מספר סקשנים (Multiple Sections)
|
||||
|
||||
**שימוש:** כותרות שונות לנספחים, עמוד לרוחב לטבלאות, שוליים שונים.
|
||||
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
// סקשן 1 — גוף ההסכם
|
||||
{
|
||||
properties: {
|
||||
page: { size: { width: 11906, height: 16838 },
|
||||
margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
|
||||
bidi: true,
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "הסכם שירותים", font: "David", size: 20, bold: true, rightToLeft: true })]
|
||||
})] })
|
||||
},
|
||||
children: [ /* ... */ ]
|
||||
},
|
||||
// סקשן 2 — נספח עם כותרת שונה
|
||||
{
|
||||
properties: {
|
||||
page: { size: { width: 11906, height: 16838 },
|
||||
margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
|
||||
bidi: true,
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START, // ✅ START
|
||||
children: [new TextRun({ text: "נספח א׳ — לוח תעריפים", font: "David", size: 20, bold: true, rightToLeft: true })]
|
||||
})] })
|
||||
},
|
||||
children: [ /* ... */ ]
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## לוגו/תמונה בכותרת (Letterhead)
|
||||
|
||||
```javascript
|
||||
const { ImageRun } = require('docx');
|
||||
|
||||
const logoBuffer = fs.readFileSync('/path/to/logo.png');
|
||||
|
||||
headers: {
|
||||
default: new Header({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: logoBuffer,
|
||||
transformation: { width: 200, height: 60 }, // pixels
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({
|
||||
text: "משרד עורכי דין ישראלי ושות׳",
|
||||
font: "David", size: 20, bold: true, rightToLeft: true
|
||||
})],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**הערה:** תמונה חייבת להיות קובץ אמיתי — לבקש מהמשתמש אם אין.
|
||||
|
||||
---
|
||||
|
||||
## היפרלינקים
|
||||
|
||||
```javascript
|
||||
const { ExternalHyperlink, UnderlineType } = require('docx');
|
||||
|
||||
new Paragraph({
|
||||
bidirectional: true,
|
||||
children: [
|
||||
new TextRun({ text: "ראה: ", font: "David", size: 24, rightToLeft: true }),
|
||||
new ExternalHyperlink({
|
||||
link: "https://www.nevo.co.il/law_html/law01/073_002.htm",
|
||||
children: [new TextRun({
|
||||
text: "חוק החוזים באתר נבו",
|
||||
font: "David", size: 24, rightToLeft: true,
|
||||
color: "0563C1",
|
||||
underline: { type: UnderlineType.SINGLE },
|
||||
})],
|
||||
}),
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**⚠️ אזהרות:**
|
||||
- **לא להשתמש ב-`style: "Hyperlink"`** — מפריע ל-RTL!
|
||||
- **לא להוסיף `alignment: AlignmentType.RIGHT`** — `bidirectional: true` מספיק
|
||||
|
||||
---
|
||||
|
||||
## תבניות מסמכים — Document Templates
|
||||
|
||||
### תבנית 1: כתב טענות (בקשה, תביעה, הגנה, ערעור)
|
||||
|
||||
```javascript
|
||||
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
|
||||
AlignmentType, LevelFormat, BorderStyle, WidthType } = require('docx');
|
||||
|
||||
const PAGE_WIDTH = 11906;
|
||||
const MARGINS = { top: 1134, right: 1134, bottom: 1134, left: 1134 };
|
||||
const CONTENT_WIDTH = PAGE_WIDTH - MARGINS.left - MARGINS.right;
|
||||
|
||||
const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
|
||||
const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder };
|
||||
|
||||
// Header בית משפט — טבלה עם שם בית המשפט (ימין) ומספר תיק (שמאל)
|
||||
function courtHeader(courtName, caseNumber) {
|
||||
return new Table({
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
|
||||
visuallyRightToLeft: true,
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
|
||||
borders: noBorders,
|
||||
children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
children: [new TextRun({ text: courtName, bold: true, font: "David", size: 26, rightToLeft: true })]
|
||||
})]
|
||||
}),
|
||||
new TableCell({
|
||||
width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
|
||||
borders: noBorders,
|
||||
children: [new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.END,
|
||||
children: [new TextRun({ text: caseNumber, bold: true, font: "David", size: 26, rightToLeft: true })]
|
||||
})]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// כותרת ראשית ממורכזת עם קו תחתון
|
||||
function mainTitle(text) {
|
||||
return new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 300, after: 300 },
|
||||
children: [new TextRun({ text, bold: true, font: "David", size: 28, rightToLeft: true, underline: {} })]
|
||||
});
|
||||
}
|
||||
|
||||
// כותרת משנה מיושרת לימין עם קו תחתון
|
||||
function subHeading(text) {
|
||||
return new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
spacing: { before: 240, after: 120 },
|
||||
children: [new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })]
|
||||
});
|
||||
}
|
||||
|
||||
// שימוש:
|
||||
const doc = new Document({
|
||||
numbering: {
|
||||
config: [{
|
||||
reference: "legal-clauses",
|
||||
levels: [{
|
||||
level: 0, format: LevelFormat.DECIMAL, text: "%1.",
|
||||
alignment: AlignmentType.START, suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 360, hanging: 360 } } }
|
||||
}]
|
||||
}]
|
||||
},
|
||||
sections: [{
|
||||
properties: {
|
||||
page: { size: { width: PAGE_WIDTH, height: 16838 }, margin: MARGINS },
|
||||
bidi: true
|
||||
},
|
||||
children: [
|
||||
courtHeader("בית המשפט המחוזי בתל אביב", "ת\"א 12345-01-26"),
|
||||
mainTitle("כתב תביעה"),
|
||||
// ... פרטי צדדים, סעיפים, חתימה
|
||||
]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### תבנית 2: מכתב התראה
|
||||
|
||||
```javascript
|
||||
// מכתב התראה — ללא header בית משפט, עם פרטי משרד
|
||||
|
||||
function letterHeader(firmName, address, phone, email) {
|
||||
return [
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
children: [new TextRun({ text: firmName, bold: true, font: "David", size: 28, rightToLeft: true })]
|
||||
}),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
children: [new TextRun({ text: address, font: "David", size: 22, rightToLeft: true })]
|
||||
}),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
spacing: { after: 300 },
|
||||
children: [new TextRun({ text: `טל': ${phone} | ${email}`, font: "David", size: 22, rightToLeft: true })]
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function subjectLine(text) {
|
||||
return new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 200, after: 200 },
|
||||
children: [
|
||||
new TextRun({ text: "הנדון: ", bold: true, font: "David", size: 24, rightToLeft: true }),
|
||||
new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// שימוש:
|
||||
sections: [{
|
||||
properties: { page: { ... }, bidi: true },
|
||||
children: [
|
||||
...letterHeader("משרד עו\"ד כהן ושות'", "רח' הרצל 1, תל אביב", "03-1234567", "office@cohen-law.co.il"),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
children: [new TextRun({ text: "תאריך: 10.2.2026", font: "David", size: 24, rightToLeft: true })]
|
||||
}),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.START,
|
||||
spacing: { before: 200 },
|
||||
children: [new TextRun({ text: "לכבוד: [שם הנמען]", font: "David", size: 24, rightToLeft: true })]
|
||||
}),
|
||||
subjectLine("התראה בטרם נקיטת הליכים משפטיים"),
|
||||
// ... גוף המכתב
|
||||
]
|
||||
}]
|
||||
```
|
||||
|
||||
### תבנית 3: הסכם/חוזה
|
||||
|
||||
```javascript
|
||||
// הסכם — הואילים, צדדים, חתימות בשני טורים
|
||||
|
||||
function contractTitle(text) {
|
||||
return new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 300 },
|
||||
children: [new TextRun({ text, bold: true, font: "David", size: 32, rightToLeft: true })]
|
||||
});
|
||||
}
|
||||
|
||||
function partyClause(label, name, id, address, alias) {
|
||||
return new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.BOTH,
|
||||
spacing: { after: 120 },
|
||||
children: [
|
||||
new TextRun({ text: `${label}: `, bold: true, font: "David", size: 24, rightToLeft: true }),
|
||||
new TextRun({ text: `${name}, ח.פ./ת.ז. ${id}, מ${address} (להלן: "`, font: "David", size: 24, rightToLeft: true }),
|
||||
new TextRun({ text: alias, bold: true, font: "David", size: 24, rightToLeft: true }),
|
||||
new TextRun({ text: '")', font: "David", size: 24, rightToLeft: true }),
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function signatureTable() {
|
||||
return new Table({
|
||||
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
|
||||
visuallyRightToLeft: true,
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
borders: noBorders,
|
||||
children: [
|
||||
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
|
||||
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "צד א'", font: "David", size: 24, rightToLeft: true })] })
|
||||
]
|
||||
}),
|
||||
new TableCell({
|
||||
borders: noBorders,
|
||||
children: [
|
||||
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
|
||||
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "צד ב'", font: "David", size: 24, rightToLeft: true })] })
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// שימוש:
|
||||
sections: [{
|
||||
properties: { page: { ... }, bidi: true },
|
||||
children: [
|
||||
contractTitle("הסכם שירותים"),
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "נערך ונחתם בתל אביב ביום __________", font: "David", size: 24, rightToLeft: true })]
|
||||
}),
|
||||
partyClause("מצד אחד", "[שם]", "[מספר]", "[כתובת]", "המזמין"),
|
||||
partyClause("מצד שני", "[שם]", "[מספר]", "[כתובת]", "הספק"),
|
||||
// הואילים...
|
||||
// סעיפים...
|
||||
new Paragraph({
|
||||
bidirectional: true, alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 400, after: 300 },
|
||||
children: [new TextRun({ text: "ולראיה באו הצדדים על החתום:", bold: true, font: "David", size: 24, rightToLeft: true })]
|
||||
}),
|
||||
signatureTable()
|
||||
]
|
||||
}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference — טבלת עזר מהיר
|
||||
|
||||
### יישור
|
||||
|
||||
| רוצה | השתמש ב... | ❌ לא להשתמש |
|
||||
|------|-----------|-------------|
|
||||
| ימין | `AlignmentType.START` | `LEFT`, `RIGHT` |
|
||||
| שמאל | `AlignmentType.END` | `LEFT`, `RIGHT` |
|
||||
| מרכז | `AlignmentType.CENTER` | — |
|
||||
| שני צדדים | `AlignmentType.BOTH` | `JUSTIFIED` |
|
||||
|
||||
### גדלי טקסט (half-points)
|
||||
|
||||
| שימוש | size | נקודות |
|
||||
|-------|------|--------|
|
||||
| גוף טקסט | 24 | 12pt |
|
||||
| כותרת משנה | 26 | 13pt |
|
||||
| כותרת ראשית | 28-32 | 14-16pt |
|
||||
| הערות שוליים | 20 | 10pt |
|
||||
| Header/Footer | 18-20 | 9-10pt |
|
||||
|
||||
### מרווחי שורות
|
||||
|
||||
| רווח | line value |
|
||||
|------|-----------|
|
||||
| 1.0 | 240 |
|
||||
| 1.15 | 276 |
|
||||
| 1.5 | 360 |
|
||||
| 2.0 | 480 |
|
||||
|
||||
### Checklist — הגדרות חובה
|
||||
|
||||
```
|
||||
☐ Section: bidi: true
|
||||
☐ Paragraph: bidirectional: true
|
||||
☐ TextRun: rightToLeft: true
|
||||
☐ Numbering: alignment: AlignmentType.START
|
||||
☐ Table: visuallyRightToLeft: true
|
||||
☐ Footnotes: alignment: AlignmentType.START
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
**מספור מופיע הפוך (.1 במקום 1.):**
|
||||
→ וודא `alignment: AlignmentType.START` במספור (לא RIGHT!)
|
||||
|
||||
**טקסט מופיע משמאל לימין:**
|
||||
→ וודא את שלושת ההגדרות: Section `bidi`, Paragraph `bidirectional`, TextRun `rightToLeft`
|
||||
|
||||
**עמודות טבלה הפוכות:**
|
||||
→ הוסף `visuallyRightToLeft: true` לטבלה
|
||||
|
||||
**columnWidths לא מסתכם:**
|
||||
→ וודא שסכום כל הרוחבים = CONTENT_WIDTH (9072 לשוליים 2.5 ס"מ, חישוב: 11906 - 1417×2)
|
||||
|
||||
**המסמך לא נפתח / שגיאה ב-Word:**
|
||||
→ בדוק שלא הוספת פסקה אחרי `<w:sectPr>` (חייב להיות אחרון ב-body)
|
||||
→ וודא `npm list docx` >= 8.0.0
|
||||
|
||||
**הערות שוליים לא ב-RTL:**
|
||||
→ אחרי unpack, תקן ידנית ב-word/footnotes.xml (ראה סעיף הערות שוליים)
|
||||
|
||||
---
|
||||
|
||||
## טעויות נפוצות — Common Pitfalls
|
||||
|
||||
| ❌ טעות | ✅ תיקון |
|
||||
|--------|---------|
|
||||
| `AlignmentType.RIGHT` במספור | `AlignmentType.START` |
|
||||
| `AlignmentType.LEFT` ליישור שמאלי | `AlignmentType.END` |
|
||||
| טבלה בלי `visuallyRightToLeft` | הוסף `visuallyRightToLeft: true` |
|
||||
| שכחת `bidirectional: true` בפסקה | הוסף לכל פסקה |
|
||||
| שכחת `rightToLeft: true` ב-TextRun | הוסף לכל TextRun |
|
||||
| שכחת `bidi: true` ב-Section | הוסף ל-properties |
|
||||
| הוספת פסקה אחרי `sectPr` | הוסף לפני `sectPr` |
|
||||
| שימוש ב-`style: "Hyperlink"` | הגדר ידנית `color` + `underline` |
|
||||
| `columnWidths` לא מסתכם נכון | וודא שהסכום = `CONTENT_WIDTH` |
|
||||
|
||||
---
|
||||
|
||||
## קבצי עזר
|
||||
|
||||
- **`references/document-types.md`** — מבנים מפורטים ל-9 סוגי מסמכים משפטיים
|
||||
- **`scripts/create-legal-doc.js`** — סקריפט בסיסי עם כל הגדרות ה-RTL המתוקנות
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```bash
|
||||
npm install docx
|
||||
```
|
||||
|
||||
**גרסה מומלצת:** docx >= 8.0.0
|
||||
271
skills/docx/references/document-types.md
Normal file
271
skills/docx/references/document-types.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# סוגי מסמכים משפטיים — מבנים ותבניות
|
||||
|
||||
## תוכן עניינים
|
||||
1. [הסכם / חוזה](#הסכם--חוזה)
|
||||
2. [מכתב התראה](#מכתב-התראה)
|
||||
3. [כתב תביעה](#כתב-תביעה)
|
||||
4. [כתב הגנה](#כתב-הגנה)
|
||||
5. [בקשה לבית משפט](#בקשה-לבית-משפט)
|
||||
6. [תצהיר](#תצהיר)
|
||||
7. [ייפוי כוח](#ייפוי-כוח)
|
||||
8. [פרוטוקול ישיבה](#פרוטוקול-ישיבה)
|
||||
9. [חוות דעת משפטית](#חוות-דעת-משפטית)
|
||||
|
||||
---
|
||||
|
||||
## הסכם / חוזה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
כותרת: "הסכם [סוג]" (מרכז, bold, 16pt)
|
||||
תאריך ומקום: "נערך ונחתם ב[עיר] ביום [תאריך]"
|
||||
צדדים:
|
||||
"מצד אחד: [שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "[כינוי]")"
|
||||
"מצד שני: [שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "[כינוי]")"
|
||||
הואילים (מוספרים):
|
||||
"1. והואיל ו[תנאי ראשון];"
|
||||
"2. והואיל ו[תנאי שני];"
|
||||
מעבר: "לפיכך הוסכם, הותנה והוצהר בין הצדדים כדלקמן:" (מרכז, bold)
|
||||
סעיפים ממוספרים: 1., 1.1, 1.2, 2., 2.1...
|
||||
חתימות: "ולראיה באו הצדדים על החתום:" + שורות חתימה
|
||||
```
|
||||
|
||||
### סעיפים נפוצים
|
||||
- היקף השירותים / הגדרות
|
||||
- תמורה ותשלום
|
||||
- תקופת ההסכם
|
||||
- סודיות
|
||||
- קניין רוחני
|
||||
- אחריות ושיפוי
|
||||
- סיום ההתקשרות
|
||||
- שונות (דין חל, סמכות שיפוט, כתובות)
|
||||
|
||||
### כותרות heading
|
||||
- כותרת ראשית: Heading1, center, 16pt
|
||||
- "בין הצדדים", "הואיל:": Heading2, center, 14pt
|
||||
- כותרות סעיפים ("1. היקף השירותים"): Heading2, right/justified, 14pt
|
||||
|
||||
---
|
||||
|
||||
## מכתב התראה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
Header: לוגו/שם משרד, כתובת, טלפון, פקס, דוא"ל
|
||||
תאריך: "ב[עיר], [תאריך]"
|
||||
סימוכין: "סימוכין: [מספר תיק]"
|
||||
נמען: "לכבוד [שם]\n[כתובת]"
|
||||
שורת נדון: "הנדון: [נושא]" (bold, underline)
|
||||
סיווג: "מכתב זה נשלח מבלי לפגוע בזכויות מרשי/תי" (bold)
|
||||
גוף: פסקאות ללא מספור — תיאור עובדות, עילה, דרישה
|
||||
סיום: "בכבוד רב," + חתימה
|
||||
העתק: "העתק: [נמענים]"
|
||||
```
|
||||
|
||||
### טון
|
||||
- פורמלי ותקיף
|
||||
- עובדתי ומדויק
|
||||
- מציין מועד אחרון לתגובה
|
||||
|
||||
---
|
||||
|
||||
## כתב תביעה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
Header: "בבית המשפט [סוג] ב[עיר]"
|
||||
מספר תיק: "ת"א [מספר]"
|
||||
צדדים:
|
||||
"התובע: [שם], ת.ז. [מספר]"
|
||||
"ע"י ב"כ עו"ד [שם], [כתובת]"
|
||||
"-נגד-"
|
||||
"הנתבע: [שם], ת.ז. [מספר]"
|
||||
כותרת: "כתב תביעה" (מרכז, bold, 16pt)
|
||||
מבוא / הצדדים
|
||||
עילות (סעיפים ממוספרים):
|
||||
1. עובדות (בפסקאות ממוספרות)
|
||||
2. הנזק
|
||||
3. העילות המשפטיות
|
||||
הסעדים המבוקשים
|
||||
חתימה
|
||||
אימות (תצהיר מצורף)
|
||||
```
|
||||
|
||||
### דגשים
|
||||
- כל עובדה בסעיף נפרד
|
||||
- הפניות לסעיפי חוק
|
||||
- סכום התביעה בסוף
|
||||
|
||||
---
|
||||
|
||||
## כתב הגנה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
Header: "בבית המשפט [סוג] ב[עיר]"
|
||||
מספר תיק: "ת"א [מספר]"
|
||||
צדדים: (כמו בכתב התביעה, עם "הנתבע/הנתבעת")
|
||||
כותרת: "כתב הגנה" (מרכז, bold, 16pt)
|
||||
מבוא
|
||||
תשובה לכתב התביעה:
|
||||
- התייחסות סעיף-סעיף ("לסעיף X — מוכחש/מאושר/אין ידיעה")
|
||||
- טענות הגנה
|
||||
- טענות מקדמיות (התיישנות, חוסר סמכות, חוסר עילה)
|
||||
סיכום
|
||||
חתימה
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## בקשה לבית משפט
|
||||
|
||||
### מבנה
|
||||
```
|
||||
Header: "בבית המשפט [סוג] ב[עיר]"
|
||||
מספר תיק
|
||||
צדדים
|
||||
כותרת: "בקשה ל[סוג הבקשה]" (מרכז, bold, 16pt)
|
||||
לדוגמה: "בקשה למתן צו מניעה זמני"
|
||||
מבוא
|
||||
הרקע העובדתי (ממוספר)
|
||||
הנימוקים המשפטיים (ממוספר)
|
||||
הסעד המבוקש
|
||||
חתימה
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## תצהיר
|
||||
|
||||
### מבנה
|
||||
```
|
||||
Header: "בבית המשפט [סוג] ב[עיר]" (אם מוגש לבימ"ש)
|
||||
כותרת: "תצהיר" / "תצהיר עדות ראשית" (מרכז, bold, 16pt)
|
||||
פתיחה: "אני, [שם], ת.ז. [מספר], לאחר שהוזהרתי כי עלי לומר את
|
||||
האמת וכי אהיה צפוי/ה לעונשים הקבועים בחוק אם לא אעשה כן,
|
||||
מצהיר/ה בזה כדלקמן:"
|
||||
גוף: סעיפים ממוספרים (עובדות בגוף ראשון)
|
||||
נספחים: "מצ"ב מסמך... מסומן כנספח [א']"
|
||||
סיום: "[שם המצהיר/ה]"
|
||||
אימות: "אישור עורך דין"
|
||||
"אני, עו"ד [שם], מאשר/ת בזה כי ביום [תאריך] התייצב/ה בפני
|
||||
[שם], שזיהה/תה עצמו/ה באמצעות ת.ז. [מספר], ולאחר שהזהרתיו/ה
|
||||
כי עליו/ה להצהיר אמת וכי יהיה/תהיה צפוי/ה לעונשים הקבועים בחוק
|
||||
אם לא יעשה/תעשה כן, אישר/ה את נכונות הצהרתו/ה וחתם/ה עליה
|
||||
בפני."
|
||||
חתימת עו"ד
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ייפוי כוח
|
||||
|
||||
### מבנה
|
||||
```
|
||||
כותרת: "ייפוי כוח" / "ייפוי כוח כללי" / "ייפוי כוח מיוחד"
|
||||
מייפה הכוח: "[שם], ת.ז. [מספר], מ[כתובת]"
|
||||
מיופה הכוח: "עו"ד [שם], רישיון [מספר]"
|
||||
ההרשאה: פירוט הפעולות המורשות
|
||||
- ייפוי כוח כללי: "לייצגני בכל עניין ולפעול בשמי..."
|
||||
- ייפוי כוח מיוחד: פירוט ספציפי של הפעולה
|
||||
תוקף: תאריך תחילה וסיום (אם רלוונטי)
|
||||
חתימה + אימות נוטריוני (אם נדרש)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## פרוטוקול ישיבה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
כותרת: "פרוטוקול ישיבת [סוג] מס' [מספר]"
|
||||
פרטים:
|
||||
"תאריך: [תאריך]"
|
||||
"שעה: [שעה]"
|
||||
"מקום: [מקום]"
|
||||
"נוכחים: [רשימה]"
|
||||
"חסרים: [רשימה]"
|
||||
"מנהל הישיבה: [שם]"
|
||||
"מזכיר: [שם]"
|
||||
סדר יום (ממוספר)
|
||||
דיון (לפי סעיפי סדר היום)
|
||||
החלטות (ממוספרות, bold)
|
||||
חתימה: מנהל הישיבה + מזכיר
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## חוות דעת משפטית
|
||||
|
||||
### מבנה
|
||||
```
|
||||
כותרת: "חוות דעת משפטית" (מרכז, bold, 16pt)
|
||||
"לכבוד: [שם הלקוח]"
|
||||
"הנדון: [נושא]"
|
||||
"סימוכין: [מספר תיק]"
|
||||
תקציר מנהלים (אופציונלי)
|
||||
רקע עובדתי
|
||||
השאלה המשפטית
|
||||
הדין החל
|
||||
ניתוח משפטי
|
||||
מסקנות והמלצות
|
||||
הסתייגויות: "חוות דעת זו מבוססת על העובדות שנמסרו לי..."
|
||||
חתימה
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## החלטת ועדת ערר לתכנון ובניה
|
||||
|
||||
### מבנה
|
||||
```
|
||||
כותרת מוסדית: טבלה ללא גבולות, 2 טורים
|
||||
ימין: "מדינת ישראל" (bold), "ועדת ערר לתכנון ובניה", "מחוז ירושלים"
|
||||
שמאל: "מס' תיק:" + מספרים (bold), מספרנו, "מס' בקשה:"/"מס' תכנית:"
|
||||
הרכב: "בפני:" (bold), יו"ר + חברים
|
||||
צדדים: "העורר/ים:" + שמות + ב"כ, "-נגד-" (מרכז, bold), "המשיבים:" + שמות + ב"כ
|
||||
כותרת: "החלטה" (16pt, bold, מרכז)
|
||||
פתיחה: 1-2 סעיפים ללא כותרת ("לפנינו...")
|
||||
פתח דבר / רקע: מקרקעין, היסטוריה תכנונית, מהות הבקשה, ציטוט מפרוטוקול, תמונות
|
||||
תמצית טענות הצדדים: (14pt, bold, קו תחתון, מרכז)
|
||||
טענות העוררים (12pt, bold, מרכז)
|
||||
עמדת הוועדה המקומית (12pt, bold, מרכז)
|
||||
עמדת מבקשי ההיתר / מגישי התכנית (12pt, bold, מרכז)
|
||||
ההליכים בפני ועדת הערר: (14pt, bold, קו תחתון, מרכז) — דיון, סיור, השלמות טיעון
|
||||
התכניות החלות על המקרקעין: (אופציונלי, 14pt, bold, קו תחתון, מרכז)
|
||||
דיון והכרעה: (14pt, bold, קו תחתון, מרכז) — אסה רציפה ללא כותרות משנה
|
||||
סיכום / סוף דבר: (14pt, bold, קו תחתון, מרכז)
|
||||
חתימות: טבלה ללא גבולות, 2 טורים (יו"ר + מזכירה)
|
||||
```
|
||||
|
||||
### דגשים
|
||||
- מספור סעיפים רציף לאורך כל המסמך (לא מתאפס בין חלקים)
|
||||
- מספר הסעיף bold (run נפרד), הטקסט רגיל
|
||||
- ציטוטים בהזחה משני הצדדים (~1 ס"מ), פונט זהה לגוף
|
||||
- תמונות ותשריטים משולבים בטקסט עם הפניה מילולית לפניהם
|
||||
- "ניתנה פה אחד, [תאריך עברי], [תאריך לועזי]." לפני חתימות
|
||||
- מספור עמודים: "עמוד X מתוך Y" תחתון מרכזי
|
||||
- **ראה** `.claude/skills/legal-decision/SKILL.md` סעיפים 11-12 למבנה מלא ותהליך עבודה
|
||||
|
||||
---
|
||||
|
||||
## עקרונות עיצוב משותפים
|
||||
|
||||
### כל מסמך משפטי
|
||||
- **פונט**: David 12pt (ברירת מחדל), FrankRuehl לפורמלי
|
||||
- **שוליים**: 2.5 ס"מ (1417 DXA) מכל הצדדים
|
||||
- **יישור**: Justified (משני צדדים)
|
||||
- **מרווח שורות**: 1.15 או 1.5 (לפי בית המשפט)
|
||||
- **מספור עמודים**: תחתון מרכזי
|
||||
- **כיוון**: RTL מלא (bidi בכל הרמות)
|
||||
|
||||
### כתבי בית דין (תביעה, הגנה, בקשה)
|
||||
- Header עם שם בית המשפט ומספר תיק
|
||||
- צדדים בפורמט סטנדרטי עם "-נגד-"
|
||||
- מספור סעיפים רציף (לא מתאפס בכל חלק)
|
||||
- Footer עם מספר עמוד
|
||||
|
||||
### הסכמים חוזיים
|
||||
- "הואילים" לפני הסעיפים
|
||||
- "ולראיה באו הצדדים על החתום" לפני חתימות
|
||||
- חתימות בשני טורים
|
||||
261
skills/docx/scripts/create-legal-doc.js
Normal file
261
skills/docx/scripts/create-legal-doc.js
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* create-legal-doc.js — תבנית בסיסית למסמך משפטי בעברית
|
||||
*
|
||||
* שימוש: העתק לתיקיית העבודה, ערוך את CONTENT, הרץ עם node.
|
||||
*
|
||||
* הסקריפט כולל את כל הגדרות ה-RTL הנדרשות ב-5 רמות:
|
||||
* 1. Document defaults (styles)
|
||||
* 2. Section properties (bidi: true)
|
||||
* 3. Per-paragraph (bidirectional + alignment)
|
||||
* 4. Per-run (rightToLeft + font)
|
||||
* 5. Numbering (alignment: START, not RIGHT!)
|
||||
*
|
||||
* שינוי פונט: החלף "David" ב-"FrankRuehl" או "Miriam"
|
||||
* שינוי שוליים: שנה MARGIN_CM
|
||||
*
|
||||
* v3.0 — תיקון באג: AlignmentType.START במקום RIGHT במספור
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const { Document, Packer, Paragraph, TextRun, Header, Footer,
|
||||
AlignmentType, HeadingLevel, PageNumber, LevelFormat,
|
||||
Table, TableRow, TableCell, WidthType, BorderStyle,
|
||||
LineRuleType } = require('docx');
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CONFIGURATION — שנה כאן
|
||||
// ═══════════════════════════════════════════════
|
||||
const FONT = "David";
|
||||
const FONT_SIZE = 24; // half-points (24 = 12pt)
|
||||
const HEADING1_SIZE = 32; // 16pt
|
||||
const HEADING2_SIZE = 28; // 14pt
|
||||
const MARGIN_CM = 2.5;
|
||||
const MARGIN_DXA = Math.round(MARGIN_CM / 2.54 * 1440);
|
||||
const OUTPUT_FILE = "legal-document.docx";
|
||||
|
||||
const HEADER_TEXT = ""; // שם המשרד — השאר ריק אם לא צריך
|
||||
const FOOTER_CONFIDENTIAL = "";
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// HELPERS — פונקציות עזר
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
// TextRun עם RTL
|
||||
const rtlRun = (text, opts = {}) => new TextRun({
|
||||
text,
|
||||
...opts,
|
||||
font: opts.font || FONT,
|
||||
size: opts.size || FONT_SIZE,
|
||||
rightToLeft: true, // ← חובה! תמיד אחרון — לא ניתן לדריסה
|
||||
});
|
||||
|
||||
// Paragraph עם RTL
|
||||
const rtlPara = (children, opts = {}) => new Paragraph({
|
||||
bidirectional: true, // ← חובה!
|
||||
alignment: opts.alignment || AlignmentType.BOTH, // ✅ BOTH, לא JUSTIFIED
|
||||
spacing: opts.spacing,
|
||||
children: Array.isArray(children) ? children : [children],
|
||||
...(opts.heading ? { heading: opts.heading } : {}),
|
||||
...(opts.numbering ? { numbering: opts.numbering } : {}),
|
||||
});
|
||||
|
||||
// כותרת ראשית
|
||||
const heading1 = (text) => rtlPara(
|
||||
rtlRun(text, { bold: true, size: HEADING1_SIZE }),
|
||||
{ heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER }
|
||||
);
|
||||
|
||||
// כותרת משנה
|
||||
const heading2 = (text) => rtlPara(
|
||||
rtlRun(text, { bold: true, size: HEADING2_SIZE }),
|
||||
{ heading: HeadingLevel.HEADING_2, alignment: AlignmentType.START } // ✅ START
|
||||
);
|
||||
|
||||
// רווח
|
||||
const spacer = (after = 200) => rtlPara(rtlRun(""), { spacing: { after } });
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CONTENT — ערוך כאן את תוכן המסמך
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
const CONTENT = [
|
||||
// כותרת
|
||||
heading1("הסכם שירותים משפטיים"),
|
||||
|
||||
// תאריך
|
||||
rtlPara(rtlRun("נערך ונחתם בתל אביב ביום ___________"), { spacing: { after: 200 } }),
|
||||
|
||||
// צדדים
|
||||
heading2("בין הצדדים"),
|
||||
|
||||
rtlPara([
|
||||
rtlRun("מצד אחד: ", { bold: true }),
|
||||
rtlRun('[שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "'),
|
||||
rtlRun("המזמין", { bold: true }),
|
||||
rtlRun('")'),
|
||||
], { spacing: { after: 120 } }),
|
||||
|
||||
rtlPara([
|
||||
rtlRun("מצד שני: ", { bold: true }),
|
||||
rtlRun('[שם], ח.פ./ת.ז. [מספר], מ[כתובת] (להלן: "'),
|
||||
rtlRun("הספק", { bold: true }),
|
||||
rtlRun('")'),
|
||||
], { spacing: { after: 200 } }),
|
||||
|
||||
// הואילים
|
||||
heading2("הואיל:"),
|
||||
rtlPara(rtlRun("1. והואיל ו[תנאי ראשון];"), { spacing: { after: 120 } }),
|
||||
rtlPara(rtlRun("2. והואיל ו[תנאי שני];"), { spacing: { after: 120 } }),
|
||||
rtlPara(rtlRun("3. והואיל והצדדים מעוניינים להסדיר את תנאי ההתקשרות ביניהם;"),
|
||||
{ spacing: { after: 200 } }),
|
||||
|
||||
// מעבר
|
||||
rtlPara(rtlRun("לפיכך הוסכם, הותנה והוצהר בין הצדדים כדלקמן:", { bold: true }),
|
||||
{ alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 } }),
|
||||
|
||||
// סעיפים
|
||||
heading2("1. היקף השירותים"),
|
||||
rtlPara(rtlRun("1.1 [תוכן הסעיף]"), { spacing: { after: 120 } }),
|
||||
rtlPara(rtlRun("1.2 [תוכן הסעיף]"), { spacing: { after: 120 } }),
|
||||
|
||||
heading2("2. התמורה"),
|
||||
rtlPara(rtlRun("2.1 [תוכן הסעיף]"), { spacing: { after: 120 } }),
|
||||
|
||||
// חתימות
|
||||
rtlPara(rtlRun("ולראיה באו הצדדים על החתום:", { bold: true }),
|
||||
{ alignment: AlignmentType.CENTER, spacing: { before: 600, after: 400 } }),
|
||||
|
||||
// טבלת חתימות
|
||||
new Table({
|
||||
visuallyRightToLeft: true, // ✅ קריטי!
|
||||
width: { size: 9072, type: WidthType.DXA },
|
||||
columnWidths: [4536, 4536],
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE },
|
||||
bottom: { style: BorderStyle.NONE },
|
||||
left: { style: BorderStyle.NONE },
|
||||
right: { style: BorderStyle.NONE }
|
||||
},
|
||||
children: [
|
||||
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER }),
|
||||
rtlPara(rtlRun("המזמין"), { alignment: AlignmentType.CENTER })
|
||||
]
|
||||
}),
|
||||
new TableCell({
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE },
|
||||
bottom: { style: BorderStyle.NONE },
|
||||
left: { style: BorderStyle.NONE },
|
||||
right: { style: BorderStyle.NONE }
|
||||
},
|
||||
children: [
|
||||
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER }),
|
||||
rtlPara(rtlRun("הספק"), { alignment: AlignmentType.CENTER })
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DOCUMENT GENERATION — לא לשנות (אלא אם יודע מה עושים)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
const doc = new Document({
|
||||
// רמה 1: Document defaults
|
||||
styles: {
|
||||
default: {
|
||||
document: {
|
||||
run: { font: FONT, size: FONT_SIZE, rightToLeft: true },
|
||||
paragraph: { bidirectional: true, alignment: AlignmentType.BOTH }
|
||||
}
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal",
|
||||
quickFormat: true,
|
||||
run: { size: HEADING1_SIZE, bold: true, font: FONT, rightToLeft: true },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0,
|
||||
bidirectional: true, alignment: AlignmentType.CENTER }
|
||||
},
|
||||
{
|
||||
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal",
|
||||
quickFormat: true,
|
||||
run: { size: HEADING2_SIZE, bold: true, font: FONT, rightToLeft: true },
|
||||
paragraph: { spacing: { before: 200, after: 200 }, outlineLevel: 1,
|
||||
bidirectional: true, alignment: AlignmentType.START } // ✅ START
|
||||
},
|
||||
]
|
||||
},
|
||||
// מספור סעיפים
|
||||
numbering: {
|
||||
config: [{
|
||||
reference: "legal-clauses",
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: "%1.",
|
||||
alignment: AlignmentType.START, // ✅ START — לא RIGHT!
|
||||
suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } }
|
||||
},
|
||||
{
|
||||
level: 1,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: "%1.%2",
|
||||
alignment: AlignmentType.START, // ✅ START — לא RIGHT!
|
||||
suffix: "tab",
|
||||
style: { paragraph: { indent: { left: 1440, hanging: 500 } } }
|
||||
},
|
||||
]
|
||||
}]
|
||||
},
|
||||
sections: [{
|
||||
// רמה 2: Section properties
|
||||
properties: {
|
||||
page: {
|
||||
size: { width: 11906, height: 16838 }, // A4
|
||||
margin: { top: MARGIN_DXA, right: MARGIN_DXA, bottom: MARGIN_DXA, left: MARGIN_DXA }
|
||||
},
|
||||
bidi: true, // ← חובה!
|
||||
},
|
||||
headers: HEADER_TEXT ? {
|
||||
default: new Header({
|
||||
children: [rtlPara(rtlRun(HEADER_TEXT, { bold: true, size: 20 }),
|
||||
{ alignment: AlignmentType.CENTER })]
|
||||
})
|
||||
} : undefined,
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [new Paragraph({
|
||||
bidirectional: true,
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
rtlRun("עמוד ", { size: 18 }),
|
||||
new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18 }),
|
||||
...(FOOTER_CONFIDENTIAL ? [rtlRun(` | ${FOOTER_CONFIDENTIAL}`, { size: 18 })] : []),
|
||||
]
|
||||
})]
|
||||
})
|
||||
},
|
||||
children: CONTENT
|
||||
}]
|
||||
});
|
||||
|
||||
Packer.toBuffer(doc).then(buffer => {
|
||||
fs.writeFileSync(OUTPUT_FILE, buffer);
|
||||
console.log(`✅ ${OUTPUT_FILE} created successfully`);
|
||||
console.log(` Font: ${FONT} ${FONT_SIZE/2}pt`);
|
||||
console.log(` Margins: ${MARGIN_CM}cm (${MARGIN_DXA} DXA)`);
|
||||
console.log(` Size: ${(buffer.length / 1024).toFixed(1)} KB`);
|
||||
console.log(` RTL: bidi + bidirectional + rightToLeft ✓`);
|
||||
console.log(` Alignment: START/END (not LEFT/RIGHT) ✓`);
|
||||
});
|
||||
41
web-ui/.gitignore
vendored
Normal file
41
web-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
5
web-ui/AGENTS.md
Normal file
5
web-ui/AGENTS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
1
web-ui/CLAUDE.md
Normal file
1
web-ui/CLAUDE.md
Normal file
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
175
web-ui/README.md
Normal file
175
web-ui/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# עוזר משפטי — Web UI (Next.js rewrite)
|
||||
|
||||
The Next.js 16 rewrite of `legal-ai.nautilus.marcusgroup.org`, currently hosted side-by-side with the legacy vanilla `index.html` at:
|
||||
|
||||
- **Staging:** https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from `ui-rewrite` branch via Coolify)
|
||||
- **Production FastAPI:** https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
|
||||
|
||||
The rewrite talks to the existing FastAPI via proxy rewrites in `next.config.ts` — no CORS setup, no duplicated backend.
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js 16.2.3 (App Router, Turbopack, `output: "standalone"`)
|
||||
- React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
|
||||
- TanStack Query v5 + TanStack Table v8
|
||||
- react-hook-form + zod for mutations
|
||||
- react-dropzone for uploads; EventSource for SSE progress
|
||||
- Heebo via `next/font/google`; design tokens in `src/app/globals.css`
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npm run build # full type check + production build
|
||||
npm run lint
|
||||
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
|
||||
```
|
||||
|
||||
### API connection
|
||||
|
||||
By default the dev server proxies to production FastAPI (`https://legal-ai.nautilus.marcusgroup.org`). To point at a different backend, set:
|
||||
|
||||
```bash
|
||||
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Route segments (App Router)
|
||||
│ ├── layout.tsx # Root: Providers + RTL html + fonts
|
||||
│ ├── page.tsx # Home: KPIs + status donut + cases table
|
||||
│ ├── error.tsx # Route-segment error boundary
|
||||
│ ├── global-error.tsx # Root crash fallback
|
||||
│ ├── not-found.tsx # 404
|
||||
│ ├── cases/
|
||||
│ │ ├── new/ # 3-step create wizard
|
||||
│ │ └── [caseNumber]/
|
||||
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
|
||||
│ │ └── compose/ # Research analysis + chair-position editor
|
||||
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
|
||||
│ ├── skills/ # Paperclip skills inventory
|
||||
│ └── diagnostics/ # DB health + failed/stuck docs
|
||||
├── components/
|
||||
│ ├── app-shell.tsx # Header + nav with aria-current
|
||||
│ ├── cases/ # Home + detail screens
|
||||
│ ├── compose/ # Research analysis editor
|
||||
│ ├── documents/ # UploadSheet
|
||||
│ ├── training/ # Style report / corpus / compare panels
|
||||
│ ├── wizard/ # Case create wizard + parties-field
|
||||
│ └── ui/ # shadcn primitives
|
||||
├── lib/
|
||||
│ ├── api/ # Typed hooks per domain (cases, documents, research,
|
||||
│ │ # system, skills, training)
|
||||
│ ├── schemas/ # zod schemas (case create / update)
|
||||
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
|
||||
│ ├── sse.ts # EventSource wrapper
|
||||
│ ├── providers.tsx # QueryClient + Toaster
|
||||
│ └── utils.ts # cn()
|
||||
```
|
||||
|
||||
## Smoke test (run after every deploy)
|
||||
|
||||
Use any browser at the staging URL. Every step should be doable **without console errors** and each mutation should produce a visible toast.
|
||||
|
||||
### 1. Home · `/`
|
||||
- [ ] Header nav shows 5 items; the current page is underlined in gold
|
||||
- [ ] 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
|
||||
- [ ] Cases table lists existing cases; search filters by case number or title
|
||||
- [ ] "פיזור סטטוסים" donut renders with a legend
|
||||
- [ ] "+ תיק חדש" button in the top-left navigates to `/cases/new`
|
||||
|
||||
### 2. Create case · `/cases/new`
|
||||
- [ ] 3-step wizard: פרטי יסוד → צדדים → השלמות
|
||||
- [ ] Type `1500-25` → appeal_subtype auto-fills to "רישוי ובנייה"
|
||||
- [ ] Type `8500-25` → subtype auto-fills to "היטל השבחה"
|
||||
- [ ] Manually pick a different subtype → auto-fill stops
|
||||
- [ ] Submitting with invalid case number shows a zod field error (no crash)
|
||||
- [ ] Successful create → toast "תיק חדש נוצר" → router pushes to `/cases/{number}`
|
||||
|
||||
### 3. Case detail · `/cases/[caseNumber]`
|
||||
- [ ] Header shows status badge + gold "ועדת ערר · X" practice-area badge
|
||||
- [ ] Tabs switch cleanly: סקירה / מסמכים / פעולות
|
||||
- [ ] Workflow timeline on the right shows the current phase highlighted in gold
|
||||
- [ ] פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
|
||||
- [ ] "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
|
||||
|
||||
### 4. Compose · `/cases/[caseNumber]/compose`
|
||||
- [ ] If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
|
||||
- [ ] Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
|
||||
- [ ] If 404 (no analysis yet): empty state card renders, no error toast
|
||||
|
||||
### 5. Training · `/training`
|
||||
- [ ] **Report tab:** headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
|
||||
- [ ] **Corpus tab:** table of corpus decisions with a trash icon per row (aria-label present)
|
||||
- [ ] Deleting a decision refreshes both the corpus table and the report KPIs
|
||||
- [ ] **Compare tab:** two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
|
||||
|
||||
### 6. Skills · `/skills`
|
||||
- [ ] Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
|
||||
- [ ] Chars + file counts render; "לא ידוע" doesn't appear for installed skills
|
||||
|
||||
### 7. Diagnostics · `/diagnostics`
|
||||
- [ ] DB status card shows "מחובר" in green
|
||||
- [ ] Table counts populate for cases / documents / chunks / corpus / patterns
|
||||
- [ ] Failed + stuck document lists render (empty states OK)
|
||||
- [ ] Page self-refreshes every 10s — check the network tab for recurring calls
|
||||
|
||||
### 8. Error boundary
|
||||
- [ ] Visit `/cases/NOT-REAL-999-99` → case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen)
|
||||
- [ ] Visit `/anything-broken-xyz` → custom 404 page with "חזרה לבית" button
|
||||
|
||||
### 9. Keyboard + RTL
|
||||
- [ ] Tab through the home page — focus rings are gold, visible
|
||||
- [ ] Wizard progresses via Enter on the "הבא" button
|
||||
- [ ] Screen reader announces nav items with "עמוד נוכחי" on the active one
|
||||
|
||||
## Deploy
|
||||
|
||||
```
|
||||
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
|
||||
```
|
||||
|
||||
> **Known issue:** the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (`mcp__coolify__deploy` with app UUID `l146g36mtlp0k03vrwkyrgkk`) or the Coolify UI until the webhook is fixed.
|
||||
|
||||
## Phase tracking
|
||||
|
||||
See `~/.claude/plans/joyful-marinating-sutton.md` for the 7-phase rewrite plan and `~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json` for the task board.
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|---|---|---|
|
||||
| 1 | Scaffold + Coolify staging | ✅ |
|
||||
| 2 | API client + typed hooks + probe | ✅ |
|
||||
| 3 | Read views (home, case detail, compose) | ✅ |
|
||||
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
|
||||
| 4.5 | Practice-area integration | ✅ |
|
||||
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
|
||||
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
|
||||
| 7 | DNS cutover to production | pending |
|
||||
|
||||
## Backend contract
|
||||
|
||||
The new UI consumes the existing FastAPI at `legal-ai/web/app.py`. Key endpoints currently relied on:
|
||||
|
||||
| Endpoint | Hook | Used by |
|
||||
|---|---|---|
|
||||
| `GET /api/cases?detail=true` | `useCases` | home table, KPIs |
|
||||
| `GET /api/cases/{n}/details` | `useCase` | case detail |
|
||||
| `POST /api/cases/create` | `useCreateCase` | wizard |
|
||||
| `PUT /api/cases/{n}` | `useUpdateCase` | inline edit |
|
||||
| `DELETE /api/cases?case_number=...` | (MCP only so far) | admin cleanup |
|
||||
| `POST /api/cases/{n}/documents/upload-tagged` | `useUploadDocument` | upload sheet |
|
||||
| `GET /api/progress/{task_id}` (SSE) | `useProgress` | upload progress |
|
||||
| `GET /api/cases/{n}/research/analysis` | `useResearchAnalysis` | compose |
|
||||
| `PATCH .../chair-position` | `useSaveChairPosition` | chair editor |
|
||||
| `GET /api/training/style-report` | `useStyleReport` | training/report tab |
|
||||
| `GET /api/training/corpus` | `useCorpus` | training/corpus tab |
|
||||
| `GET /api/training/compare` | `useCompare` | training/compare tab |
|
||||
| `DELETE /api/training/corpus/{id}` | `useDeleteCorpusEntry` | corpus tab |
|
||||
| `GET /api/system/diagnostics` | `useDiagnostics` | diagnostics page |
|
||||
| `GET /api/admin/skills` | `useSkills` | skills page |
|
||||
|
||||
Any new endpoint should get a typed hook in `src/lib/api/` — do not reach into `fetch` from component code.
|
||||
25
web-ui/components.json
Normal file
25
web-ui/components.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": true,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user