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) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 21:05:53 +00:00
parent e2088a4f60
commit 50eaa887db
7 changed files with 772 additions and 0 deletions

240
docs/corpus-analysis.md Normal file
View 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 החלטות של היטל השבחה לפני שהמערכת יכולה לכתוב החלטות בתחום הזה.

View File

@@ -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")

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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 ─────────────────────────────────────────