Add outcome-aware drafting, lessons system, and improved style analysis

- Add expected_outcome field to cases (rejection/partial/full/betterment_levy)
- New lessons.py module with golden ratios, templates, and drafting guidance per outcome type
- Style analyzer now uses Opus with full decision text (no truncation), with multi-pass fallback for large corpora
- Drafting tool provides outcome-specific templates, section guidance, and ratio comments
- Improved JSON extraction with bracket-matching fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 18:58:42 +00:00
parent 6f515dc2cb
commit 39089dcef5
6 changed files with 726 additions and 71 deletions

View File

@@ -0,0 +1,331 @@
"""Lessons learned from comparing AI drafts to Dafna Tamir's final decisions.
Source: /data/uploads/לקחים-לעדכון-שרת-כתיבת-החלטות.md
Based on analysis of: Hecht 1180-1181 (rejection) and Beit HaKerem 1126/25+1141/25 (partial acceptance).
"""
from __future__ import annotations
# ── Valid outcome values ────────────────────────────────────────────
VALID_OUTCOMES = ("rejection", "partial_acceptance", "full_acceptance", "betterment_levy")
# ── Golden Ratios (section % of total) ─────────────────────────────
GOLDEN_RATIOS: dict[str, dict[str, tuple[int, int]]] = {
"rejection": {"background": (15, 25), "claims": (30, 40), "discussion": (37, 50), "summary": (2, 9)},
"full_acceptance": {"background": (30, 40), "claims": (20, 30), "discussion": (35, 45), "summary": (3, 5)},
"partial_acceptance": {"background": (25, 35), "claims": (25, 30), "discussion": (40, 47), "summary": (2, 3)},
"betterment_levy": {"background": (6, 18), "claims": (13, 25), "discussion": (32, 48), "summary": (3, 4)},
}
# ── Paragraph length guidance (word counts) ────────────────────────
PARAGRAPH_LENGTHS = {
"claims": (40, 60),
"discussion_regular": (40, 80),
"discussion_with_citation": (200, 600),
"discussion_average": (80, 120),
}
# ── Transition phrases ─────────────────────────────────────────────
TRANSITION_PHRASES = [
# From Hecht (rejection)
{"phrase": "ועל מנת לא לצאת בחסר", "context": "פתיחת obiter dicta", "outcome": None},
{"phrase": "נציין כי טענות אלו נטענו בלשון רפה", "context": "הכרה בטענות חלשות", "outcome": None},
{"phrase": "עינינו הרואות", "context": "סיכום אחרי ציטוט ארוך", "outcome": None},
{"phrase": "נוסיף.", "context": "מעבר חד (מילה אחת)", "outcome": None},
{"phrase": "אם כך, לעת הזו", "context": "מסקנה מציטוטים", "outcome": None},
{"phrase": "למיטב הבנתנו", "context": "עמדה זהירה", "outcome": None},
{"phrase": "נשלים ונציין", "context": "נקודה אחרונה לפני סיכום", "outcome": None},
# From Beit HaKerem (partial acceptance)
{"phrase": "הדברים משליכים על שיקול הדעת ב...", "context": "קישור ממצא למסקנה", "outcome": "partial_acceptance"},
{"phrase": "רוצה לומר כי", "context": "הסבר חלופי", "outcome": None},
{"phrase": "נוצר מצב בו", "context": "הצגת בעיה", "outcome": None},
{"phrase": "לכך נוסיף כי", "context": "הוספת נדבך", "outcome": None},
{"phrase": "יש אולי להצר על כך ש...", "context": "ביקורת עדינה", "outcome": None},
{"phrase": "עם ההבנה לטענה זו של העוררים, אין בידנו לקבלה", "context": "acknowledge-reject מרוכך", "outcome": None},
]
# ── Opening strategies by outcome ──────────────────────────────────
OPENING_STRATEGIES = {
"rejection": {
"style": "broad_contextual",
"paragraphs": (5, 8),
"description": "פתיחה רחבה — הקשר תכנוני כללי, רקע לפני צלילה לטענות",
},
"full_acceptance": {
"style": "direct_conclusion",
"paragraphs": (1, 2),
"description": "פתיחה ישירה — ישר למסקנה, תמציתית",
},
"partial_acceptance": {
"style": "tension_mapping",
"paragraphs": (3, 6),
"description": (
"מיפוי מתחים — 1-2 פסקאות על ערך התכנון, "
"אחר כך 'בערר דנן עולות שאלות כיצד והאם...' "
"עם רשימת 4-6 נקודות מתח בבולטים, "
"ואז 'כל הנקודות לעיל עומדות לפנינו...' → מעבר לניתוח"
),
},
"betterment_levy": {
"style": "direct_with_disclaimer",
"paragraphs": (1, 3),
"description": "פתיחה ישירה עם מסקנה + 'על מנת לא לצאת בחסר'",
},
}
# ── Summary strategies by outcome ──────────────────────────────────
SUMMARY_STRATEGIES = {
"rejection": {
"heading": "סיכום",
"format": "numbered_hebrew_with_warm_closing",
"description": "אותיות עבריות (א-ו) עם פירוט נימוקים + פסקת סיום חמה",
},
"full_acceptance": {
"heading": "סוף דבר",
"format": "prose_paragraphs",
"description": "פרוזה (3-5 פסקאות), ללא פסקה חמה",
},
"partial_acceptance": {
"heading": "סוף דבר",
"format": "ultra_minimal_operative",
"description": (
"אולטרה-מינימלי: 2-3 הוראות אופרטיביות בלבד. "
"אפס חזרה על נימוקים. אפס הוצאות. אפס סיום חם. "
"כל ההנמקה כבר בדיון — הסיכום = רק מה מתקבל, מה נדחה, ותנאים"
),
},
"betterment_levy": {
"heading": "סיכום",
"format": "numbered_hebrew_dry",
"description": "אותיות עבריות, סיום יבש ללא פסקה חמה",
},
}
# ── Discussion structure rules ─────────────────────────────────────
DISCUSSION_RULES: dict[str, list[str]] = {
"universal": [
"פרק הדיון = אסה רציפה. אין כותרות משנה (H2/H3). מעברים רק עם ביטויי מעבר טקסטואליים.",
"חריג יחיד לכותרות משנה: נושאים נפרדים לחלוטין (למשל: הקלה בגובה + התייחסות לטענות נוספות).",
"טווח אורך סעיפים: 20 עד 600+ מילים. סעיף עם ציטוט מקיף = בלוק אחד שלם, לא שבירה לסעיפים קצרים.",
],
"rejection": [
"מבנה עיגולים קונצנטריים: שכבות הגנה — סף (ס' 152) → מריט → obiter dicta.",
"שאלת הסף (ס' 152) = כלי אסטרטגי, לא חובה. משתמשים בה כשהיא חזקה.",
],
"partial_acceptance": [
"מבנה: מיפוי מתחים → ניתוח נושא-נושא → הוראות אופרטיביות.",
"שאלת הסף (ס' 152) — בדרך כלל מדלגים. כשיש שאלות מהותיות חזקות (חניה, שימור, קווי בניין), דפנה מעדיפה דיון בגוף העניין.",
"דפוס 'בית בודד': כשתמ\"א 38 חלה על בית בודד, אינטרס החיזוק מוחלש → שיקול דעת זהיר יותר.",
"דפוס 'תכנית אב כמגן': כשקיימת תכנית אב → לצטט אותה → ההיתר 'משתלב עם ראיה כללית'.",
],
"full_acceptance": [
"מבנה ישיר: נקודות עיקריות → ניתוח → מסקנה.",
],
"betterment_levy": [
"מבנה ישיר עם מסקנה מוקדמת + 'על מנת לא לצאת בחסר' לנקודות נוספות.",
],
}
# ── Citation technique ─────────────────────────────────────────────
CITATION_GUIDANCE = (
"העדפה לציטוט דרך 'החלטה מרכזת' — החלטה אחת שכבר ריכזה את הפסיקה הרלוונטית. "
"דפוס: 'נפנה לניתוח המקיף שערכה ועדת הערר במסגרת ערר [שם]...' → בלוק ציטוט 200-500 מילים → 'אם כך, לעת הזו...'. "
"גמישות: כשיש שאלות משפטיות מרובות או חדשניות, כל נושא עשוי לדרוש תקדים נפרד. "
"בנושאי חניה/תשתיות — צלילה לעומק: ציטוט ישיר של הוראות תכנית (400+ מילים עם ניתוח שזור)."
)
# ── Decision templates by outcome ──────────────────────────────────
_HEADER = """# החלטה
## בפני: דפנה תמיר, יו"ר ועדת הערר מחוז ירושלים
**ערר מספר:** {case_number}
**נושא:** {subject}
**העוררים:** {appellants}
**המשיבים:** {respondents}
**כתובת הנכס:** {property_address}
---
"""
DECISION_TEMPLATES: dict[str, str] = {
"rejection": _HEADER + """## א. רקע עובדתי
<!-- {ratios_background} -->
[תיאור הרקע העובדתי של הערר]
## ב. טענות העוררים
<!-- {ratios_claims} -->
[סיכום טענות העוררים]
## ג. טענות המשיבים
[סיכום טענות המשיבים]
## ד. דיון
<!-- אסה רציפה, ללא כותרות משנה. מבנה עיגולים קונצנטריים: סף → מריט → obiter -->
<!-- פתיחה רחבה: 5-8 פסקאות הקשר תכנוני -->
<!-- {ratios_discussion} -->
[ניתוח משפטי — אסה רציפה]
## ה. סיכום
<!-- אותיות עבריות (א-ו) + פסקת סיום חמה -->
<!-- {ratios_summary} -->
[סיכום בפורמט רשימה ממוספרת + סיום חם]
---
ניתנה היום, {date}
דפנה תמיר, יו"ר ועדת הערר
""",
"partial_acceptance": _HEADER + """## א. רקע עובדתי
<!-- {ratios_background} -->
[תיאור הרקע העובדתי של הערר]
## ב. טענות העוררים
<!-- {ratios_claims} -->
[סיכום טענות העוררים]
## ג. טענות המשיבים
[סיכום טענות המשיבים]
## ד. דיון
<!-- אסה רציפה, ללא כותרות משנה. מבנה: מיפוי מתחים → ניתוח נושא-נושא -->
<!-- פתיחת מיפוי מתחים: 1-2 פסקאות על ערך התכנון, רשימת מתחים, מעבר לניתוח -->
<!-- {ratios_discussion} -->
[ניתוח משפטי — אסה רציפה]
## ה. סוף דבר
<!-- אולטרה-מינימלי: 2-3 הוראות אופרטיביות בלבד. אפס נימוקים. אפס הוצאות. -->
<!-- {ratios_summary} -->
[הוראות אופרטיביות בלבד: מה מתקבל, מה נדחה, תנאים]
---
ניתנה היום, {date}
דפנה תמיר, יו"ר ועדת הערר
""",
"full_acceptance": _HEADER + """## א. רקע עובדתי
<!-- {ratios_background} -->
[תיאור הרקע העובדתי של הערר]
## ב. טענות העוררים
<!-- {ratios_claims} -->
[סיכום טענות העוררים]
## ג. טענות המשיבים
[סיכום טענות המשיבים]
## ד. דיון
<!-- אסה רציפה, ללא כותרות משנה. מבנה ישיר -->
<!-- פתיחה ישירה: 1-2 פסקאות, ישר למסקנה -->
<!-- {ratios_discussion} -->
[ניתוח משפטי — אסה רציפה]
## ה. סוף דבר
<!-- פרוזה: 3-5 פסקאות, ללא פסקה חמה -->
<!-- {ratios_summary} -->
[סוף דבר בפרוזה]
---
ניתנה היום, {date}
דפנה תמיר, יו"ר ועדת הערר
""",
"betterment_levy": _HEADER + """## א. רקע עובדתי
<!-- {ratios_background} -->
[תיאור הרקע העובדתי של הערר]
## ב. טענות העוררים
<!-- {ratios_claims} -->
[סיכום טענות העוררים]
## ג. טענות המשיבים
[סיכום טענות המשיבים]
## ד. דיון
<!-- אסה רציפה, ללא כותרות משנה. מבנה ישיר עם מסקנה מוקדמת -->
<!-- {ratios_discussion} -->
[ניתוח משפטי — אסה רציפה]
## ה. סיכום
<!-- אותיות עבריות, סיום יבש -->
<!-- {ratios_summary} -->
[סיכום בפורמט רשימה ממוספרת, סיום יבש]
---
ניתנה היום, {date}
דפנה תמיר, יו"ר ועדת הערר
""",
}
# ── Helper function ────────────────────────────────────────────────
def get_lessons_for_outcome(outcome: str) -> dict:
"""Assemble all relevant lessons for a given expected outcome."""
if outcome not in VALID_OUTCOMES:
return {"error": f"outcome must be one of: {', '.join(VALID_OUTCOMES)}"}
ratios = GOLDEN_RATIOS[outcome]
rules = DISCUSSION_RULES.get("universal", []) + DISCUSSION_RULES.get(outcome, [])
# Filter transition phrases: universal + outcome-specific
phrases = [
p for p in TRANSITION_PHRASES
if p["outcome"] is None or p["outcome"] == outcome
]
return {
"outcome": outcome,
"golden_ratios": {
k: f"{v[0]}-{v[1]}%" for k, v in ratios.items()
},
"opening_strategy": OPENING_STRATEGIES[outcome],
"summary_strategy": SUMMARY_STRATEGIES[outcome],
"discussion_rules": rules,
"citation_guidance": CITATION_GUIDANCE,
"transition_phrases": [
{"phrase": p["phrase"], "context": p["context"]}
for p in phrases
],
"paragraph_lengths": {
k: f"{v[0]}-{v[1]} מילים" for k, v in PARAGRAPH_LENGTHS.items()
},
}
def format_ratios_comment(outcome: str, section: str) -> str:
"""Format golden ratio as an HTML comment for templates."""
ratios = GOLDEN_RATIOS.get(outcome, {})
if section in ratios:
lo, hi = ratios[section]
return f"יעד: {lo}-{hi}% מסך ההחלטה"
return ""