From 0fef20e2725dae17b719b67eca6a54db8ad7ec3a Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 12 Apr 2026 20:58:28 +0000 Subject: [PATCH] Add content checklists for block-yod and chair feedback system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Dafna's observation that licensing decisions lack comprehensive planning discussion. Systematic corpus analysis of all 24 training decisions revealed the system learned writing style but not substantive content. Changes: - Corpus analysis of all 24 decisions (docs/corpus-analysis.md) - 5 content checklists by appeal subtype injected into block-yod prompt - chair_feedback DB table + API endpoints + MCP tools - Feedback management page in Next.js UI (/feedback) - Navigation updated with "הערות יו״ר" link Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/corpus-analysis.md | 240 +++++++++++++ mcp-server/src/legal_mcp/server.py | 23 ++ .../src/legal_mcp/services/block_writer.py | 13 + mcp-server/src/legal_mcp/services/db.py | 85 +++++ mcp-server/src/legal_mcp/services/lessons.py | 190 ++++++++++ mcp-server/src/legal_mcp/tools/workflow.py | 94 +++++ web-ui/src/app/feedback/page.tsx | 329 ++++++++++++++++++ web-ui/src/components/app-shell.tsx | 1 + web-ui/src/lib/api/feedback.ts | 112 ++++++ web/app.py | 127 +++++++ 10 files changed, 1214 insertions(+) create mode 100644 docs/corpus-analysis.md create mode 100644 web-ui/src/app/feedback/page.tsx create mode 100644 web-ui/src/lib/api/feedback.ts diff --git a/docs/corpus-analysis.md b/docs/corpus-analysis.md new file mode 100644 index 0000000..4543228 --- /dev/null +++ b/docs/corpus-analysis.md @@ -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 החלטות של היטל השבחה לפני שהמערכת יכולה לכתוב החלטות בתחום הזה. diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index 54dc127..d158bd2 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -346,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") diff --git a/mcp-server/src/legal_mcp/services/block_writer.py b/mcp-server/src/legal_mcp/services/block_writer.py index 08599e5..091973d 100644 --- a/mcp-server/src/legal_mcp/services/block_writer.py +++ b/mcp-server/src/legal_mcp/services/block_writer.py @@ -20,6 +20,7 @@ from uuid import UUID from legal_mcp import config from legal_mcp.services import db, embeddings, claude_session +from legal_mcp.services.lessons import get_content_checklist logger = logging.getLogger(__name__) @@ -215,6 +216,8 @@ BLOCK_PROMPTS = { - **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין) - מספור רציף +{content_checklist} + ## כיוון מאושר (חובה): {direction_context} @@ -310,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%" @@ -323,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 diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 1f8a4a5..cb3df66 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -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 -- ═══════════════════════════════════════════════════════════════════ @@ -986,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, + ) diff --git a/mcp-server/src/legal_mcp/services/lessons.py b/mcp-server/src/legal_mcp/services/lessons.py index 1868945..ee5eafe 100644 --- a/mcp-server/src/legal_mcp/services/lessons.py +++ b/mcp-server/src/legal_mcp/services/lessons.py @@ -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"] diff --git a/mcp-server/src/legal_mcp/tools/workflow.py b/mcp-server/src/legal_mcp/tools/workflow.py index 90b9a06..00735ad 100644 --- a/mcp-server/src/legal_mcp/tools/workflow.py +++ b/mcp-server/src/legal_mcp/tools/workflow.py @@ -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) diff --git a/web-ui/src/app/feedback/page.tsx b/web-ui/src/app/feedback/page.tsx new file mode 100644 index 0000000..1d5be98 --- /dev/null +++ b/web-ui/src/app/feedback/page.tsx @@ -0,0 +1,329 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { AppShell } from "@/components/app-shell"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + useFeedbackList, + useCreateFeedback, + useResolveFeedback, + CATEGORY_LABELS, + BLOCK_LABELS, + type FeedbackCategory, +} from "@/lib/api/feedback"; +import { toast } from "sonner"; + +const CATEGORY_COLORS: Record = { + missing_content: "bg-amber-100 text-amber-800 border-amber-200", + wrong_tone: "bg-purple-100 text-purple-800 border-purple-200", + wrong_structure: "bg-blue-100 text-blue-800 border-blue-200", + factual_error: "bg-red-100 text-red-800 border-red-200", + style: "bg-emerald-100 text-emerald-800 border-emerald-200", + other: "bg-gray-100 text-gray-800 border-gray-200", +}; + +export default function FeedbackPage() { + const [showResolved, setShowResolved] = useState(false); + const [filterCategory, setFilterCategory] = useState(""); + + const { data: feedbacks, isLoading } = useFeedbackList({ + category: filterCategory || undefined, + unresolved_only: !showResolved, + }); + + const resolveMutation = useResolveFeedback(); + + function handleResolve(id: string) { + resolveMutation.mutate( + { feedbackId: id, applied_to: [] }, + { + onSuccess: () => toast.success("ההערה סומנה כמטופלת"), + onError: () => toast.error("שגיאה בעדכון"), + }, + ); + } + + return ( + +
+
+ +

הערות יו״ר על טיוטות

+

+ תיעוד הערות דפנה על טיוטות החלטות. כל הערה מנותחת ומשפיעה על שיפור + כתיבת ההחלטות העתידיות. +

+
+ +
+ + {/* Toolbar */} +
+ + + + + + + {feedbacks && ( + + {feedbacks.length} הערות + + )} +
+ + {/* Feedback list */} + {isLoading ? ( + + + טוען... + + + ) : !feedbacks?.length ? ( + + + אין הערות{!showResolved ? " פתוחות" : ""} + {filterCategory ? ` בקטגוריה ${CATEGORY_LABELS[filterCategory as FeedbackCategory]}` : ""} + + + ) : ( +
+ {feedbacks.map((fb) => ( + + +
+ + {CATEGORY_LABELS[fb.category]} + + + {BLOCK_LABELS[fb.block_id] ?? fb.block_id} + + {fb.case_number && ( + + תיק {fb.case_number} + + )} + {fb.resolved && ( + + טופל + + )} + + {fb.created_at + ? new Date(fb.created_at).toLocaleDateString("he-IL") + : ""} + +
+
+ +

{fb.feedback_text}

+ + {fb.lesson_extracted && ( +
+

+ לקח שהופק: +

+

+ {fb.lesson_extracted} +

+
+ )} + + {!fb.resolved && ( +
+ +
+ )} +
+
+ ))} +
+ )} +
+
+ ); +} + +/* ── New feedback dialog ─────────────────────────────────── */ + +function NewFeedbackDialog() { + const [open, setOpen] = useState(false); + const createMutation = useCreateFeedback(); + + const [caseNumber, setCaseNumber] = useState(""); + const [blockId, setBlockId] = useState("block-yod"); + const [category, setCategory] = useState("missing_content"); + const [feedbackText, setFeedbackText] = useState(""); + const [lesson, setLesson] = useState(""); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!feedbackText.trim()) return; + + createMutation.mutate( + { + case_number: caseNumber || undefined, + block_id: blockId, + feedback_text: feedbackText, + category, + lesson_extracted: lesson || undefined, + }, + { + onSuccess: () => { + toast.success("ההערה נרשמה בהצלחה"); + setOpen(false); + setCaseNumber(""); + setFeedbackText(""); + setLesson(""); + }, + onError: () => toast.error("שגיאה ברישום ההערה"), + }, + ); + } + + return ( + + + + + + + רישום הערת יו״ר + +
+
+
+ + setCaseNumber(e.target.value)} + placeholder="1130-25" + /> +
+
+ + +
+
+ +
+ + +
+ +
+ +