From 50eaa887dbbc928f30ab7cb3d1759cb4e24faac3 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 12 Apr 2026 21:05:53 +0000 Subject: [PATCH] Add chair feedback system and content checklists for block-yod Backend changes cherry-picked from ui-rewrite branch to enable feedback API endpoints for the Next.js staging UI. - chair_feedback DB table + API endpoints (GET/POST/PATCH) - Content checklists by appeal subtype injected into block-yod prompt - MCP tools for recording and listing chair feedback - Corpus analysis documentation (24 decisions) 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/app.py | 127 +++++++++ 7 files changed, 772 insertions(+) create mode 100644 docs/corpus-analysis.md 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 06363d5..2f6b732 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -390,6 +390,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 94e05be..889555e 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 64f2b61..83bf408 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -462,6 +462,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 -- ═══════════════════════════════════════════════════════════════════ @@ -1321,3 +1337,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/app.py b/web/app.py index ab7906d..e352a54 100644 --- a/web/app.py +++ b/web/app.py @@ -2436,6 +2436,133 @@ async def api_reprocess_document(case_number: str, doc_id: str): return {"status": "reprocessing"} +# ── Chair feedback endpoints ────────────────────────────────────── + + +@app.get("/api/feedback") +async def api_list_feedback( + case_number: str = "", + category: str = "", + unresolved_only: bool = False, +): + """List chair feedback, optionally filtered by case/category.""" + 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, + ) + + items = [] + # Build case_number lookup + case_numbers: dict[str, str] = {} + pool = await db.get_pool() + for fb in feedbacks: + cid = fb.get("case_id") + cn = "" + if cid and str(cid) not in case_numbers: + async with pool.acquire() as conn: + row = await conn.fetchrow( + "SELECT case_number, title FROM cases WHERE id = $1", cid, + ) + if row: + case_numbers[str(cid)] = row["case_number"] + if cid: + cn = case_numbers.get(str(cid), "") + + items.append({ + "id": str(fb["id"]), + "case_id": str(fb["case_id"]) if fb["case_id"] else None, + "case_number": cn, + "block_id": fb["block_id"], + "category": fb["category"], + "feedback_text": fb["feedback_text"], + "lesson_extracted": fb["lesson_extracted"], + "resolved": fb["resolved"], + "applied_to": fb.get("applied_to", []), + "created_at": fb["created_at"].isoformat() if fb.get("created_at") else None, + }) + + return items + + +@app.post("/api/feedback") +async def api_create_feedback( + case_number: str = Form(""), + block_id: str = Form("block-yod"), + feedback_text: str = Form(...), + category: str = Form("missing_content"), + lesson_extracted: str = Form(""), +): + """Record a new chair feedback entry.""" + case_id = None + if case_number: + case = await db.get_case_by_number(case_number) + if case: + case_id = UUID(case["id"]) + + valid_categories = [ + "missing_content", "wrong_tone", "wrong_structure", + "factual_error", "style", "other", + ] + if category not in valid_categories: + raise HTTPException(400, 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 {"id": str(feedback_id), "status": "created"} + + +@app.post("/api/feedback/json") +async def api_create_feedback_json(body: dict): + """Record a new chair feedback entry (JSON body).""" + case_number = body.get("case_number", "") + case_id = None + if case_number: + case = await db.get_case_by_number(case_number) + if case: + case_id = UUID(case["id"]) + + valid_categories = [ + "missing_content", "wrong_tone", "wrong_structure", + "factual_error", "style", "other", + ] + category = body.get("category", "missing_content") + if category not in valid_categories: + raise HTTPException(400, f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}") + + feedback_id = await db.record_chair_feedback( + case_id=case_id, + block_id=body.get("block_id", "block-yod"), + feedback_text=body.get("feedback_text", ""), + category=category, + lesson_extracted=body.get("lesson_extracted", ""), + ) + + return {"id": str(feedback_id), "status": "created"} + + +@app.patch("/api/feedback/{feedback_id}/resolve") +async def api_resolve_feedback(feedback_id: str, body: dict): + """Mark feedback as resolved.""" + await db.resolve_chair_feedback( + feedback_id=UUID(feedback_id), + applied_to=body.get("applied_to", []), + ) + return {"status": "resolved"} + + # ── Background Processing ─────────────────────────────────────────