Merge pull request 'feat(mcp): FU-14 GAP-51 — איחוד אוצר-המילים של תוצאת-תיק (set_outcome SSoT)' (#65) from fix/fu14-gap51-outcome-ssot-impl into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m41s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m41s
This commit was merged in pull request #65.
This commit is contained in:
@@ -83,9 +83,10 @@ Microsoft *REST API Guidelines* (pagination) · Stripe API (limit caps) | סטט
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 3. הערות-עיצוב
|
## 3. הערות-עיצוב
|
||||||
- **set_outcome** — אי-התאמת enum: `block_writer.py:442` משתמש ב-`rejected/accepted/partial`, בעוד
|
- **set_outcome — GAP-51 ✅ נסגר (2026-06-06):** SSoT יחיד = 3 תוצאות קנוניות `rejection/partial_acceptance/full_acceptance`
|
||||||
`lessons.py:11` מגדיר `rejection/partial_acceptance/full_acceptance/betterment_levy`. שתי אוצרות-מילים →
|
ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` = מפת-תוויות עברית אחת (אנגלית ב-DB, עברית ב-UI); `canonical_outcome()`
|
||||||
SSoT יחיד נדרש ([gap-audit GAP-51](gap-audit.md); תואם [X6 INV-UI1](X6-ui-api-contract.md) על enum-סטטוס).
|
ממפה ערכי-legacy (rejected/accepted/partial). `betterment_levy` הוצא מהיותו תוצאה → `PRACTICE_AREA_OVERRIDES`
|
||||||
|
(override לפי practice_area מעל התוצאה). נתונים נורמלו (~9 שורות, גיבוי ב-`data/audit/gap51-outcome-backup-*`).
|
||||||
- **3 מסלולי-קליטת-פסיקה** (library / internal / training) עם ולידציה א-סימטרית — נקשר ל-[01-ingest.md](01-ingest.md) / GAP-01/05.
|
- **3 מסלולי-קליטת-פסיקה** (library / internal / training) עם ולידציה א-סימטרית — נקשר ל-[01-ingest.md](01-ingest.md) / GAP-01/05.
|
||||||
|
|
||||||
הממצאים המלאים + התיקון → **FU-14** ([gap-audit.md](gap-audit.md)).
|
הממצאים המלאים + התיקון → **FU-14** ([gap-audit.md](gap-audit.md)).
|
||||||
|
|||||||
@@ -199,7 +199,8 @@
|
|||||||
- **מכסה:** GAP-44,45,47..54 · **invariants:** INV-TOOL1–TOOL5 · **effort:** L · **תלויות:** FU-1
|
- **מכסה:** GAP-44,45,47..54 · **invariants:** INV-TOOL1–TOOL5 · **effort:** L · **תלויות:** FU-1
|
||||||
- **סוג:** code — envelope אחיד, מיזוג חיפוש/בלוקים, idempotency, limit-caps, get-symmetry, set_outcome SSoT
|
- **סוג:** code — envelope אחיד, מיזוג חיפוש/בלוקים, idempotency, limit-caps, get-symmetry, set_outcome SSoT
|
||||||
- **סטטוס חלקי (פרוסה 1, 2026-06-06):** ✅ **GAP-44** — נוסף `get_appraiser_facts` (ה-get המקביל ל-extract, INV-TOOL4); ✅ **GAP-53** — נוסף `_clamp_limit` (תקרה 200, INV-TOOL5) על ~13 כלי list/search + הוספת limit ל-`list_chair_feedback` (שהיה ללא תקרה).
|
- **סטטוס חלקי (פרוסה 1, 2026-06-06):** ✅ **GAP-44** — נוסף `get_appraiser_facts` (ה-get המקביל ל-extract, INV-TOOL4); ✅ **GAP-53** — נוסף `_clamp_limit` (תקרה 200, INV-TOOL5) על ~13 כלי list/search + הוספת limit ל-`list_chair_feedback` (שהיה ללא תקרה).
|
||||||
- **סטטוס חלקי (פרוסה 2, 2026-06-06):** ✅ **GAP-52** (INV-TOOL3 idempotency) — `case_create`/`precedent_attach`/`document_upload` מחזירים קיים במקום כפילות (בדיקת-מפתח ברמת-אפליקציה; document_upload לפי SHA-256 → מדלג OCR/embed כפול); ✅ **GAP-45** (INV-TOOL4 visibility) — נוסף `extraction_status` שחושף עומק תור-החילוץ (metadata/halacha) + גיל הבקשה הוותיקה. נותר: GAP-51 (set_outcome enum SSoT — דורש הכרעת-domain), GAP-48 (envelope), GAP-49/50 (מיזוג+rename — שובר).
|
- **סטטוס חלקי (פרוסה 2, 2026-06-06):** ✅ **GAP-52** (INV-TOOL3 idempotency) — `case_create`/`precedent_attach`/`document_upload` מחזירים קיים במקום כפילות (בדיקת-מפתח ברמת-אפליקציה; document_upload לפי SHA-256 → מדלג OCR/embed כפול); ✅ **GAP-45** (INV-TOOL4 visibility) — נוסף `extraction_status` שחושף עומק תור-החילוץ (metadata/halacha) + גיל הבקשה הוותיקה.
|
||||||
|
- **סטטוס חלקי (פרוסה 3, 2026-06-06):** ✅ **GAP-51** (set_outcome SSoT, הכרעת-יו"ר "3 תוצאות + הוצאת betterment_levy") — קנוני `rejection/partial_acceptance/full_acceptance` ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` (עברית-ב-UI SSoT); `canonical_outcome()` ל-legacy; `betterment_levy`→`PRACTICE_AREA_OVERRIDES` (override לפי practice_area); block_writer/set_outcome/drafting/web-ui יושרו; נתונים נורמלו (9 שורות + גיבוי). נותר ב-FU-14: GAP-48 (envelope), GAP-49/50 (מיזוג+rename — שובר).
|
||||||
|
|
||||||
### FU-15 — deploy/env/secrets
|
### FU-15 — deploy/env/secrets
|
||||||
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ async def case_update(
|
|||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
expected_outcome: str = "",
|
expected_outcome: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
|
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance (betterment_levy הוא practice_area, לא תוצאה)."""
|
||||||
return await cases.case_update(
|
return await cases.case_update(
|
||||||
case_number, status, title, subject, notes,
|
case_number, status, title, subject, notes,
|
||||||
hearing_date, decision_date, tags, expected_outcome,
|
hearing_date, decision_date, tags, expected_outcome,
|
||||||
@@ -680,7 +680,7 @@ async def set_outcome(
|
|||||||
outcome: str,
|
outcome: str,
|
||||||
reasoning: str = "",
|
reasoning: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הזנת תוצאה לתיק: rejected (דחייה), accepted (קבלה), partial (קבלה חלקית). אם אין נימוק — מפעיל סיעור מוחות."""
|
"""הזנת תוצאה לתיק: rejection (דחייה), partial_acceptance (קבלה חלקית), full_acceptance (קבלה מלאה). ערכי-legacy ממופים. אם אין נימוק — מפעיל סיעור מוחות."""
|
||||||
return await workflow.set_outcome(case_number, outcome, reasoning)
|
return await workflow.set_outcome(case_number, outcome, reasoning)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ from uuid import UUID
|
|||||||
|
|
||||||
from legal_mcp import config
|
from legal_mcp import config
|
||||||
from legal_mcp.services import db, embeddings, claude_session, audit
|
from legal_mcp.services import db, embeddings, claude_session, audit
|
||||||
from legal_mcp.services.lessons import get_content_checklist, get_methodology_summary
|
from legal_mcp.services.lessons import (
|
||||||
|
OUTCOME_LABELS_HE,
|
||||||
|
PRACTICE_AREA_OVERRIDES,
|
||||||
|
canonical_outcome,
|
||||||
|
get_content_checklist,
|
||||||
|
get_methodology_summary,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -270,10 +276,11 @@ BLOCK_PROMPTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Discussion structure by outcome
|
# Discussion structure by outcome
|
||||||
|
# GAP-51: keyed by canonical outcomes (rejection/partial_acceptance/full_acceptance).
|
||||||
STRUCTURE_GUIDANCE = {
|
STRUCTURE_GUIDANCE = {
|
||||||
"rejected": "דחייה — שכבות הגנה (concentric circles): טענה ראשית → נדחית, טענה חלופית → נדחית, חיזוק.",
|
"rejection": "דחייה — שכבות הגנה (concentric circles): טענה ראשית → נדחית, טענה חלופית → נדחית, חיזוק.",
|
||||||
"accepted": "קבלה — נימוק-נימוק: כל נימוק = CREAC מלא, בניית שכנוע הדרגתי.",
|
"full_acceptance": "קבלה — נימוק-נימוק: כל נימוק = CREAC מלא, בניית שכנוע הדרגתי.",
|
||||||
"partial": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.",
|
"partial_acceptance": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -327,8 +334,13 @@ async def write_block(
|
|||||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||||
|
|
||||||
outcome = (decision or {}).get("outcome", "rejected")
|
outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
|
||||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||||
|
if case.get("practice_area") == "betterment_levy":
|
||||||
|
structure_guidance = (
|
||||||
|
structure_guidance + " | היטל השבחה: "
|
||||||
|
+ " ".join(PRACTICE_AREA_OVERRIDES["betterment_levy"]["discussion_rules"])
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Content checklist — tells block-yod WHAT topics to cover
|
# Content checklist — tells block-yod WHAT topics to cover
|
||||||
content_checklist = ""
|
content_checklist = ""
|
||||||
@@ -438,8 +450,8 @@ async def _collect_block_sources(case_id: UUID, block_id: str) -> dict:
|
|||||||
# ── Context builders ──────────────────────────────────────────────
|
# ── Context builders ──────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_case_context(case: dict, decision: dict | None) -> str:
|
def _build_case_context(case: dict, decision: dict | None) -> str:
|
||||||
outcome = (decision or {}).get("outcome", "")
|
outcome = canonical_outcome((decision or {}).get("outcome", ""))
|
||||||
outcome_heb = {"rejected": "דחייה", "accepted": "קבלה", "partial": "קבלה חלקית"}.get(outcome, "")
|
outcome_heb = OUTCOME_LABELS_HE.get(outcome, "")
|
||||||
return f"""- מספר תיק: {case['case_number']}
|
return f"""- מספר תיק: {case['case_number']}
|
||||||
- כותרת: {case.get('title', '')}
|
- כותרת: {case.get('title', '')}
|
||||||
- עוררים: {', '.join(case.get('appellants', []))}
|
- עוררים: {', '.join(case.get('appellants', []))}
|
||||||
@@ -877,8 +889,13 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
|||||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||||
|
|
||||||
outcome = (decision or {}).get("outcome", "rejected")
|
outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
|
||||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||||
|
if case.get("practice_area") == "betterment_levy":
|
||||||
|
structure_guidance = (
|
||||||
|
structure_guidance + " | היטל השבחה: "
|
||||||
|
+ " ".join(PRACTICE_AREA_OVERRIDES["betterment_levy"]["discussion_rules"])
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Content checklist + methodology for block-yod
|
# Content checklist + methodology for block-yod
|
||||||
content_checklist = ""
|
content_checklist = ""
|
||||||
|
|||||||
@@ -7,8 +7,32 @@ Based on analysis of: Hecht 1180-1181 (rejection) and Beit HaKerem 1126/25+1141/
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# ── Valid outcome values ────────────────────────────────────────────
|
# ── Valid outcome values ────────────────────────────────────────────
|
||||||
|
# GAP-51 / INV-TOOL2: canonical = 3 real outcomes. `betterment_levy` is a
|
||||||
|
# practice_area (not an outcome) — its writing-guidance lives in
|
||||||
|
# PRACTICE_AREA_OVERRIDES below and is applied on top of the chosen outcome.
|
||||||
|
|
||||||
VALID_OUTCOMES = ("rejection", "partial_acceptance", "full_acceptance", "betterment_levy")
|
VALID_OUTCOMES = ("rejection", "partial_acceptance", "full_acceptance")
|
||||||
|
|
||||||
|
# Hebrew display labels — SSoT (אנגלית ב-DB, עברית ב-UI). Replaces the inline
|
||||||
|
# maps that lived in block_writer.py and workflow.py.
|
||||||
|
OUTCOME_LABELS_HE = {
|
||||||
|
"rejection": "דחייה",
|
||||||
|
"partial_acceptance": "קבלה חלקית",
|
||||||
|
"full_acceptance": "קבלה מלאה",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backward-compat: legacy set_outcome vocabulary → canonical. Used by callers
|
||||||
|
# that may still pass the old values (rejected/accepted/partial).
|
||||||
|
LEGACY_OUTCOME_MAP = {
|
||||||
|
"rejected": "rejection",
|
||||||
|
"accepted": "full_acceptance",
|
||||||
|
"partial": "partial_acceptance",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_outcome(outcome: str) -> str:
|
||||||
|
"""Normalize any outcome string to the canonical vocabulary (GAP-51)."""
|
||||||
|
return LEGACY_OUTCOME_MAP.get(outcome, outcome)
|
||||||
|
|
||||||
# ── Golden Ratios (section % of total) ─────────────────────────────
|
# ── Golden Ratios (section % of total) ─────────────────────────────
|
||||||
|
|
||||||
@@ -16,7 +40,6 @@ GOLDEN_RATIOS: dict[str, dict[str, tuple[int, int]]] = {
|
|||||||
"rejection": {"background": (15, 25), "claims": (30, 40), "discussion": (37, 50), "summary": (2, 9)},
|
"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)},
|
"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)},
|
"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 length guidance (word counts) ────────────────────────
|
||||||
@@ -71,16 +94,6 @@ OPENING_STRATEGIES = {
|
|||||||
"ואז 'כל הנקודות לעיל עומדות לפנינו...' → מעבר לניתוח"
|
"ואז 'כל הנקודות לעיל עומדות לפנינו...' → מעבר לניתוח"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"betterment_levy": {
|
|
||||||
"style": "direct_factual",
|
|
||||||
"paragraphs": (1, 3),
|
|
||||||
"description": (
|
|
||||||
"פתיחה ישירה ועובדתית: 'בפנינו ערר על דרישת תשלום היטל השבחה מיום [תאריך] "
|
|
||||||
"בסך של [סכום] ₪' → רקע קצר (נכס, תכנית משביחה, מימוש) → "
|
|
||||||
"תמצית טענות הצדדים (עוררים + משיבה בנפרד). "
|
|
||||||
"אין הקשר תכנוני רחב. הפתיחה = עובדות בלבד."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Summary strategies by outcome ──────────────────────────────────
|
# ── Summary strategies by outcome ──────────────────────────────────
|
||||||
@@ -105,18 +118,6 @@ SUMMARY_STRATEGIES = {
|
|||||||
"כל ההנמקה כבר בדיון — הסיכום = רק מה מתקבל, מה נדחה, ותנאים"
|
"כל ההנמקה כבר בדיון — הסיכום = רק מה מתקבל, מה נדחה, ותנאים"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"betterment_levy": {
|
|
||||||
"heading": "various",
|
|
||||||
"format": "dry_operative",
|
|
||||||
"description": (
|
|
||||||
"סיום יבש ואופרטיבי. כותרת משתנה: 'סוף דבר' / 'לאור כל האמור לעיל' / ללא כותרת. "
|
|
||||||
"תוכן: 'הערר נדחה/מתקבל' + הוצאות ('כל צד ישא בהוצאותיו' / חיוב בסכום). "
|
|
||||||
"אם מתקבל: הוראות אופרטיביות (החזר, שומה מתוקנת, תנאים). "
|
|
||||||
"חתימה: 'ניתנה פה אחד היום, [תאריך עברי], [תאריך לועזי].' "
|
|
||||||
"לעיתים: 'התיק ייסגר.' / 'עומדת זכות ערר כדין.' "
|
|
||||||
"אין פסקה חמה. אין חזרה על נימוקים."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Discussion structure rules ─────────────────────────────────────
|
# ── Discussion structure rules ─────────────────────────────────────
|
||||||
@@ -140,14 +141,6 @@ DISCUSSION_RULES: dict[str, list[str]] = {
|
|||||||
"full_acceptance": [
|
"full_acceptance": [
|
||||||
"מבנה ישיר: נקודות עיקריות → ניתוח → מסקנה.",
|
"מבנה ישיר: נקודות עיקריות → ניתוח → מסקנה.",
|
||||||
],
|
],
|
||||||
"betterment_levy": [
|
|
||||||
"פתיחת דיון: מסקנה מוקדמת ('לאחר שבחנו... מצאנו כי דין הערר להידחות/להתקבל').",
|
|
||||||
"תקן ביקורת: ציון רף ההתערבות בשומה מכרעת (בר\"ם 3644/13 גלר) — אבחנה בין שמאי למשפטי.",
|
|
||||||
"הצגת הלכה פסוקה: ציטוט ארוך מפס\"ד מרכזי → 'ברוח הדברים לעיל נבחן את טענות הצדדים'.",
|
|
||||||
"טיפול שיטתי: כל טענה/סוגיה בנפרד → ניתוח → מסקנת ביניים.",
|
|
||||||
"ביטויים: 'אין בידינו לקבל', 'לא מצאנו מקום להתערב', 'קביעה נכונה שאין מקום להתערב בה'.",
|
|
||||||
"'על מנת לא לצאת בחסר' — לנקודות obiter dicta בסוף הדיון.",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Citation technique ─────────────────────────────────────────────
|
# ── Citation technique ─────────────────────────────────────────────
|
||||||
@@ -270,8 +263,49 @@ DECISION_TEMPLATES: dict[str, str] = {
|
|||||||
ניתנה היום, {date}
|
ניתנה היום, {date}
|
||||||
דפנה תמיר, יו"ר ועדת הערר
|
דפנה תמיר, יו"ר ועדת הערר
|
||||||
""",
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
"betterment_levy": _HEADER + """## א. רקע עובדתי
|
|
||||||
|
# ── Practice-area writing overrides (GAP-51) ───────────────────────
|
||||||
|
# `betterment_levy` is a practice_area, NOT an outcome. A betterment-levy case
|
||||||
|
# still has a real outcome (rejection / partial / full), but its writing style
|
||||||
|
# is distinct (dry, factual, no warm closing). These overrides are layered on
|
||||||
|
# top of the chosen outcome's guidance by the accessors below.
|
||||||
|
|
||||||
|
PRACTICE_AREA_OVERRIDES: dict[str, dict] = {
|
||||||
|
"betterment_levy": {
|
||||||
|
"golden_ratios": {"background": (6, 18), "claims": (13, 25), "discussion": (32, 48), "summary": (3, 4)},
|
||||||
|
"opening_strategy": {
|
||||||
|
"style": "direct_factual",
|
||||||
|
"paragraphs": (1, 3),
|
||||||
|
"description": (
|
||||||
|
"פתיחה ישירה ועובדתית: 'בפנינו ערר על דרישת תשלום היטל השבחה מיום [תאריך] "
|
||||||
|
"בסך של [סכום] ₪' → רקע קצר (נכס, תכנית משביחה, מימוש) → "
|
||||||
|
"תמצית טענות הצדדים (עוררים + משיבה בנפרד). "
|
||||||
|
"אין הקשר תכנוני רחב. הפתיחה = עובדות בלבד."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"summary_strategy": {
|
||||||
|
"heading": "various",
|
||||||
|
"format": "dry_operative",
|
||||||
|
"description": (
|
||||||
|
"סיום יבש ואופרטיבי. כותרת משתנה: 'סוף דבר' / 'לאור כל האמור לעיל' / ללא כותרת. "
|
||||||
|
"תוכן: 'הערר נדחה/מתקבל' + הוצאות ('כל צד ישא בהוצאותיו' / חיוב בסכום). "
|
||||||
|
"אם מתקבל: הוראות אופרטיביות (החזר, שומה מתוקנת, תנאים). "
|
||||||
|
"חתימה: 'ניתנה פה אחד היום, [תאריך עברי], [תאריך לועזי].' "
|
||||||
|
"לעיתים: 'התיק ייסגר.' / 'עומדת זכות ערר כדין.' "
|
||||||
|
"אין פסקה חמה. אין חזרה על נימוקים."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"discussion_rules": [
|
||||||
|
"פתיחת דיון: מסקנה מוקדמת ('לאחר שבחנו... מצאנו כי דין הערר להידחות/להתקבל').",
|
||||||
|
"תקן ביקורת: ציון רף ההתערבות בשומה מכרעת (בר\"ם 3644/13 גלר) — אבחנה בין שמאי למשפטי.",
|
||||||
|
"הצגת הלכה פסוקה: ציטוט ארוך מפס\"ד מרכזי → 'ברוח הדברים לעיל נבחן את טענות הצדדים'.",
|
||||||
|
"טיפול שיטתי: כל טענה/סוגיה בנפרד → ניתוח → מסקנת ביניים.",
|
||||||
|
"ביטויים: 'אין בידינו לקבל', 'לא מצאנו מקום להתערב', 'קביעה נכונה שאין מקום להתערב בה'.",
|
||||||
|
"'על מנת לא לצאת בחסר' — לנקודות obiter dicta בסוף הדיון.",
|
||||||
|
],
|
||||||
|
"decision_template": _HEADER + """## א. רקע עובדתי
|
||||||
<!-- {ratios_background} -->
|
<!-- {ratios_background} -->
|
||||||
|
|
||||||
[תיאור הרקע העובדתי של הערר]
|
[תיאור הרקע העובדתי של הערר]
|
||||||
@@ -301,18 +335,31 @@ DECISION_TEMPLATES: dict[str, str] = {
|
|||||||
ניתנה היום, {date}
|
ניתנה היום, {date}
|
||||||
דפנה תמיר, יו"ר ועדת הערר
|
דפנה תמיר, יו"ר ועדת הערר
|
||||||
""",
|
""",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Helper function ────────────────────────────────────────────────
|
# ── Helper function ────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_lessons_for_outcome(outcome: str) -> dict:
|
def get_lessons_for_outcome(outcome: str, practice_area: str = "") -> dict:
|
||||||
"""Assemble all relevant lessons for a given expected outcome."""
|
"""Assemble all relevant lessons for an outcome, with practice_area overrides.
|
||||||
|
|
||||||
|
GAP-51: ``betterment_levy`` is a practice_area — when given, its writing
|
||||||
|
overrides (golden ratios, opening/summary strategy, discussion rules) are
|
||||||
|
layered on top of the chosen outcome.
|
||||||
|
"""
|
||||||
|
outcome = canonical_outcome(outcome)
|
||||||
if outcome not in VALID_OUTCOMES:
|
if outcome not in VALID_OUTCOMES:
|
||||||
return {"error": f"outcome must be one of: {', '.join(VALID_OUTCOMES)}"}
|
return {"error": f"outcome must be one of: {', '.join(VALID_OUTCOMES)}"}
|
||||||
|
|
||||||
ratios = GOLDEN_RATIOS[outcome]
|
override = PRACTICE_AREA_OVERRIDES.get(practice_area, {})
|
||||||
rules = DISCUSSION_RULES.get("universal", []) + DISCUSSION_RULES.get(outcome, [])
|
ratios = override.get("golden_ratios") or GOLDEN_RATIOS[outcome]
|
||||||
|
opening = override.get("opening_strategy") or OPENING_STRATEGIES[outcome]
|
||||||
|
summary = override.get("summary_strategy") or SUMMARY_STRATEGIES[outcome]
|
||||||
|
rules = (
|
||||||
|
DISCUSSION_RULES.get("universal", [])
|
||||||
|
+ (override.get("discussion_rules") or DISCUSSION_RULES.get(outcome, []))
|
||||||
|
)
|
||||||
|
|
||||||
# Filter transition phrases: universal + outcome-specific
|
# Filter transition phrases: universal + outcome-specific
|
||||||
phrases = [
|
phrases = [
|
||||||
@@ -322,11 +369,12 @@ def get_lessons_for_outcome(outcome: str) -> dict:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"outcome": outcome,
|
"outcome": outcome,
|
||||||
|
"practice_area": practice_area,
|
||||||
"golden_ratios": {
|
"golden_ratios": {
|
||||||
k: f"{v[0]}-{v[1]}%" for k, v in ratios.items()
|
k: f"{v[0]}-{v[1]}%" for k, v in ratios.items()
|
||||||
},
|
},
|
||||||
"opening_strategy": OPENING_STRATEGIES[outcome],
|
"opening_strategy": opening,
|
||||||
"summary_strategy": SUMMARY_STRATEGIES[outcome],
|
"summary_strategy": summary,
|
||||||
"discussion_rules": rules,
|
"discussion_rules": rules,
|
||||||
"citation_guidance": CITATION_GUIDANCE,
|
"citation_guidance": CITATION_GUIDANCE,
|
||||||
"transition_phrases": [
|
"transition_phrases": [
|
||||||
@@ -339,9 +387,11 @@ def get_lessons_for_outcome(outcome: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_ratios_comment(outcome: str, section: str) -> str:
|
def format_ratios_comment(outcome: str, section: str, practice_area: str = "") -> str:
|
||||||
"""Format golden ratio as an HTML comment for templates."""
|
"""Format golden ratio as an HTML comment for templates (practice_area-aware)."""
|
||||||
ratios = GOLDEN_RATIOS.get(outcome, {})
|
outcome = canonical_outcome(outcome)
|
||||||
|
override = PRACTICE_AREA_OVERRIDES.get(practice_area, {})
|
||||||
|
ratios = override.get("golden_ratios") or GOLDEN_RATIOS.get(outcome, {})
|
||||||
if section in ratios:
|
if section in ratios:
|
||||||
lo, hi = ratios[section]
|
lo, hi = ratios[section]
|
||||||
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ from legal_mcp.services.lessons import (
|
|||||||
GOLDEN_RATIOS,
|
GOLDEN_RATIOS,
|
||||||
OPENING_STRATEGIES,
|
OPENING_STRATEGIES,
|
||||||
PARAGRAPH_LENGTHS,
|
PARAGRAPH_LENGTHS,
|
||||||
|
PRACTICE_AREA_OVERRIDES,
|
||||||
SUMMARY_STRATEGIES,
|
SUMMARY_STRATEGIES,
|
||||||
TRANSITION_PHRASES,
|
TRANSITION_PHRASES,
|
||||||
VALID_OUTCOMES,
|
VALID_OUTCOMES,
|
||||||
|
canonical_outcome,
|
||||||
format_ratios_comment,
|
format_ratios_comment,
|
||||||
get_lessons_for_outcome,
|
get_lessons_for_outcome,
|
||||||
)
|
)
|
||||||
@@ -156,6 +158,15 @@ async def get_style_guide() -> str:
|
|||||||
f"| {r['discussion'][0]}-{r['discussion'][1]}% "
|
f"| {r['discussion'][0]}-{r['discussion'][1]}% "
|
||||||
f"| {r['summary'][0]}-{r['summary'][1]}% |\n"
|
f"| {r['summary'][0]}-{r['summary'][1]}% |\n"
|
||||||
)
|
)
|
||||||
|
# GAP-51: betterment_levy is a practice_area override (applied on top of the outcome), not an outcome.
|
||||||
|
_bl = PRACTICE_AREA_OVERRIDES["betterment_levy"]["golden_ratios"]
|
||||||
|
result += (
|
||||||
|
f"| {outcome_labels['betterment_levy']} (override לפי תחום) "
|
||||||
|
f"| {_bl['background'][0]}-{_bl['background'][1]}% "
|
||||||
|
f"| {_bl['claims'][0]}-{_bl['claims'][1]}% "
|
||||||
|
f"| {_bl['discussion'][0]}-{_bl['discussion'][1]}% "
|
||||||
|
f"| {_bl['summary'][0]}-{_bl['summary'][1]}% |\n"
|
||||||
|
)
|
||||||
result += "\n"
|
result += "\n"
|
||||||
|
|
||||||
# Opening and summary strategies
|
# Opening and summary strategies
|
||||||
@@ -167,6 +178,13 @@ async def get_style_guide() -> str:
|
|||||||
result += f"- **פתיחה:** {opening['description']} ({opening['paragraphs'][0]}-{opening['paragraphs'][1]} פסקאות)\n"
|
result += f"- **פתיחה:** {opening['description']} ({opening['paragraphs'][0]}-{opening['paragraphs'][1]} פסקאות)\n"
|
||||||
result += f"- **סיכום ({summary['heading']}):** {summary['description']}\n\n"
|
result += f"- **סיכום ({summary['heading']}):** {summary['description']}\n\n"
|
||||||
|
|
||||||
|
# GAP-51: betterment_levy override (practice_area, applied over the outcome)
|
||||||
|
_bo = PRACTICE_AREA_OVERRIDES["betterment_levy"]
|
||||||
|
_op, _sm = _bo["opening_strategy"], _bo["summary_strategy"]
|
||||||
|
result += f"### {outcome_labels['betterment_levy']} (override לפי תחום)\n"
|
||||||
|
result += f"- **פתיחה:** {_op['description']} ({_op['paragraphs'][0]}-{_op['paragraphs'][1]} פסקאות)\n"
|
||||||
|
result += f"- **סיכום ({_sm['heading']}):** {_sm['description']}\n\n"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -319,7 +337,9 @@ async def get_decision_template(case_number: str) -> str:
|
|||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return f"תיק {case_number} לא נמצא."
|
||||||
|
|
||||||
expected_outcome = case.get("expected_outcome", "")
|
# GAP-51: canonicalize outcome + apply betterment_levy practice_area override.
|
||||||
|
expected_outcome = canonical_outcome(case.get("expected_outcome", ""))
|
||||||
|
practice_area = case.get("practice_area", "")
|
||||||
|
|
||||||
format_args = dict(
|
format_args = dict(
|
||||||
case_number=case["case_number"],
|
case_number=case["case_number"],
|
||||||
@@ -332,19 +352,24 @@ async def get_decision_template(case_number: str) -> str:
|
|||||||
|
|
||||||
# Use outcome-specific template if available
|
# Use outcome-specific template if available
|
||||||
if expected_outcome and expected_outcome in DECISION_TEMPLATES:
|
if expected_outcome and expected_outcome in DECISION_TEMPLATES:
|
||||||
# Add ratio comments
|
# Add ratio comments (practice_area-aware)
|
||||||
format_args["ratios_background"] = format_ratios_comment(expected_outcome, "background")
|
format_args["ratios_background"] = format_ratios_comment(expected_outcome, "background", practice_area)
|
||||||
format_args["ratios_claims"] = format_ratios_comment(expected_outcome, "claims")
|
format_args["ratios_claims"] = format_ratios_comment(expected_outcome, "claims", practice_area)
|
||||||
format_args["ratios_discussion"] = format_ratios_comment(expected_outcome, "discussion")
|
format_args["ratios_discussion"] = format_ratios_comment(expected_outcome, "discussion", practice_area)
|
||||||
format_args["ratios_summary"] = format_ratios_comment(expected_outcome, "summary")
|
format_args["ratios_summary"] = format_ratios_comment(expected_outcome, "summary", practice_area)
|
||||||
|
|
||||||
template = DECISION_TEMPLATES[expected_outcome].format(**format_args)
|
# betterment_levy practice_area supplies its own template; else use the outcome's.
|
||||||
|
override = PRACTICE_AREA_OVERRIDES.get(practice_area, {})
|
||||||
|
template_src = override.get("decision_template") or DECISION_TEMPLATES[expected_outcome]
|
||||||
|
template = template_src.format(**format_args)
|
||||||
|
|
||||||
# Add guidance header
|
# Add guidance header (override-aware via get_lessons_for_outcome)
|
||||||
opening = OPENING_STRATEGIES[expected_outcome]
|
lessons_o = get_lessons_for_outcome(expected_outcome, practice_area)
|
||||||
summary = SUMMARY_STRATEGIES[expected_outcome]
|
opening = lessons_o["opening_strategy"]
|
||||||
|
summary = lessons_o["summary_strategy"]
|
||||||
header = (
|
header = (
|
||||||
f"<!-- תבנית מותאמת ל: {expected_outcome} -->\n"
|
f"<!-- תבנית מותאמת ל: {expected_outcome}"
|
||||||
|
f"{' / ' + practice_area if practice_area in PRACTICE_AREA_OVERRIDES else ''} -->\n"
|
||||||
f"<!-- פתיחת דיון: {opening['description']} -->\n"
|
f"<!-- פתיחת דיון: {opening['description']} -->\n"
|
||||||
f"<!-- סיכום: {summary['description']} -->\n\n"
|
f"<!-- סיכום: {summary['description']} -->\n\n"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import logging
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from legal_mcp.services import db
|
from legal_mcp.services import db
|
||||||
|
from legal_mcp.services.lessons import (
|
||||||
|
OUTCOME_LABELS_HE,
|
||||||
|
VALID_OUTCOMES,
|
||||||
|
canonical_outcome,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -151,7 +156,8 @@ async def set_outcome(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
case_number: מספר תיק הערר
|
case_number: מספר תיק הערר
|
||||||
outcome: תוצאה — rejected (דחייה), accepted (קבלה), partial (קבלה חלקית)
|
outcome: תוצאה — rejection (דחייה) / partial_acceptance (קבלה חלקית) /
|
||||||
|
full_acceptance (קבלה מלאה). ערכי-legacy (rejected/accepted/partial) ממופים אוטומטית.
|
||||||
reasoning: נימוק (אופציונלי). אם ריק — מפעיל סיעור מוחות.
|
reasoning: נימוק (אופציונלי). אם ריק — מפעיל סיעור מוחות.
|
||||||
"""
|
"""
|
||||||
from legal_mcp.services import brainstorm
|
from legal_mcp.services import brainstorm
|
||||||
@@ -160,9 +166,10 @@ async def set_outcome(
|
|||||||
if not case:
|
if not case:
|
||||||
return f"תיק {case_number} לא נמצא."
|
return f"תיק {case_number} לא נמצא."
|
||||||
|
|
||||||
valid_outcomes = ("rejected", "accepted", "partial")
|
# GAP-51: accept legacy vocabulary (rejected/accepted/partial), store canonical.
|
||||||
if outcome not in valid_outcomes:
|
outcome = canonical_outcome(outcome)
|
||||||
return f"תוצאה לא תקינה. אפשרויות: {', '.join(valid_outcomes)}"
|
if outcome not in VALID_OUTCOMES:
|
||||||
|
return f"תוצאה לא תקינה. אפשרויות: {', '.join(VALID_OUTCOMES)}"
|
||||||
|
|
||||||
case_id = UUID(case["id"])
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
@@ -187,7 +194,7 @@ async def set_outcome(
|
|||||||
# Update case status
|
# Update case status
|
||||||
await db.update_case(case_id, status="in_progress", expected_outcome=outcome)
|
await db.update_case(case_id, status="in_progress", expected_outcome=outcome)
|
||||||
|
|
||||||
outcome_hebrew = {"rejected": "דחייה", "accepted": "קבלה", "partial": "קבלה חלקית"}.get(outcome, outcome)
|
outcome_hebrew = OUTCOME_LABELS_HE.get(outcome, outcome)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"decision_id": decision["id"],
|
"decision_id": decision["id"],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|--------|------|---------|-----------|
|
|--------|------|---------|-----------|
|
||||||
| `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh <METHOD> <PATH> [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים |
|
| `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh <METHOD> <PATH> [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים |
|
||||||
| `spec-guard.sh` | bash | **PreToolUse hook לאכיפת "פרוטוקול כתיבת-קוד"** (CLAUDE.md §פרוטוקול כתיבת-קוד) — בכל Edit/Write/MultiEdit על נתיב-קוד (`web/`, `mcp-server/`, `web-ui/src/`, `scripts/`, `adapters/`) מזריק תזכורת ל-Claude לקרוא את `docs/spec/00-constitution.md`+ספ-התחום ולוודא קיום G1–G11 — לפני שכותבים. המקבילה האינטראקטיבית ל-INV-AG1 (שאוכף על סוכני Paperclip ב-HEARTBEAT.md §"קריאת-ספ"). קלט JSON ב-stdin (`.tool_input.file_path`), פלט `hookSpecificOutput.additionalContext` (non-blocking, exit 0). מחריג `.md`/`docs/`/`tests/`/artifacts. Dedup פעם-בסשן (`$TMPDIR/.spec-guard-<session_id>`). רשום ב-`.claude/settings.json`. | נקרא אוטומטית ע"י Claude Code (hook) |
|
| `spec-guard.sh` | bash | **PreToolUse hook לאכיפת "פרוטוקול כתיבת-קוד"** (CLAUDE.md §פרוטוקול כתיבת-קוד) — בכל Edit/Write/MultiEdit על נתיב-קוד (`web/`, `mcp-server/`, `web-ui/src/`, `scripts/`, `adapters/`) מזריק תזכורת ל-Claude לקרוא את `docs/spec/00-constitution.md`+ספ-התחום ולוודא קיום G1–G11 — לפני שכותבים. המקבילה האינטראקטיבית ל-INV-AG1 (שאוכף על סוכני Paperclip ב-HEARTBEAT.md §"קריאת-ספ"). קלט JSON ב-stdin (`.tool_input.file_path`), פלט `hookSpecificOutput.additionalContext` (non-blocking, exit 0). מחריג `.md`/`docs/`/`tests/`/artifacts. Dedup פעם-בסשן (`$TMPDIR/.spec-guard-<session_id>`). רשום ב-`.claude/settings.json`. | נקרא אוטומטית ע"י Claude Code (hook) |
|
||||||
|
| `migrate_gap51_outcomes.py` | python | **GAP-51 (FU-14)** — נרמול ערכי `outcome` לאוצר הקנוני (rejected→rejection, accepted→full_acceptance, partial→partial_acceptance) ב-`decisions.outcome` + `cases.expected_outcome`. `betterment_levy` לא ממופה (practice_area, לא outcome). `--dry-run` (ברירת-מחדל) / `--apply` (גיבוי ל-`data/audit/gap51-outcome-backup-*.csv` + UPDATE טרנזקציוני). דורש POSTGRES_URL. בוצע 2026-06-06 (9 שורות). נוגע רק ב-cases/decisions — בטוח במקביל לחילוץ. | חד-פעמי (בוצע) |
|
||||||
| `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס |
|
| `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס |
|
||||||
| `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — `--apply` מדלג על הסוכן; `--verify` מדווח אותו רם כ-DRIFT.** בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות. **`--verify` יוצא exit≠0 על כל drift** (needs-sync / adapter-mismatch / missing-in-mirror) — שמיש כ-gate ל-cron/CI (GAP-21/FU-8a). | ידני אחרי כל שינוי |
|
| `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — `--apply` מדלג על הסוכן; `--verify` מדווח אותו רם כ-DRIFT.** בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות. **`--verify` יוצא exit≠0 על כל drift** (needs-sync / adapter-mismatch / missing-in-mirror) — שמיש כ-gate ל-cron/CI (GAP-21/FU-8a). | ידני אחרי כל שינוי |
|
||||||
| `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) |
|
| `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) |
|
||||||
@@ -73,6 +74,8 @@
|
|||||||
| `run_curator_deepseek_test.sh` | A/B test #1 (2026-05-05) — Hermes Curator על CMP-78 דרך DeepSeek V4-Pro ב-`provider:custom`, ללא interaction. תוצאה: 6:33 דק׳, 5 ממצאי סגנון/לקסיקון, פי 3 מהיר מ-Sonnet baseline (CMP-80) ופי ~20 זול. **הסקריפט נקודתי לתיק 1130-25 — לא להריץ שוב** | החלפת Curator לאדפטר DeepSeek מקומי (בתהליך) |
|
| `run_curator_deepseek_test.sh` | A/B test #1 (2026-05-05) — Hermes Curator על CMP-78 דרך DeepSeek V4-Pro ב-`provider:custom`, ללא interaction. תוצאה: 6:33 דק׳, 5 ממצאי סגנון/לקסיקון, פי 3 מהיר מ-Sonnet baseline (CMP-80) ופי ~20 זול. **הסקריפט נקודתי לתיק 1130-25 — לא להריץ שוב** | החלפת Curator לאדפטר DeepSeek מקומי (בתהליך) |
|
||||||
| `run_curator_deepseek_test_v2.sh` | A/B test #2 (2026-05-05) — אותו run אבל עם interaction. תוצאה: 9:08 דק׳, 5 ממצאים, היחיד מ-4 הריצות שזיהה תוצאה עובדתית נכונה (קבלה חלקית). interaction נכשל ב-API ("Agent run id required" בריצה ידנית). | החלפת Curator לאדפטר DeepSeek מקומי |
|
| `run_curator_deepseek_test_v2.sh` | A/B test #2 (2026-05-05) — אותו run אבל עם interaction. תוצאה: 9:08 דק׳, 5 ממצאים, היחיד מ-4 הריצות שזיהה תוצאה עובדתית נכונה (קבלה חלקית). interaction נכשל ב-API ("Agent run id required" בריצה ידנית). | החלפת Curator לאדפטר DeepSeek מקומי |
|
||||||
| `run_curator_sonnet_rerun.sh` | A/B test #3 (2026-05-05) — ריצה חוזרת של Sonnet 4.5 על אותו CMP-78. תוצאה: 12:52 דק׳ (לעומת 20:13 בריצה המקורית — כי בלי לולאת interaction.json). זיהה תוצאה שגויה ("דחייה") **בעקביות עם הריצה המקורית** — Sonnet עקבי-בטעות, DeepSeek אקראי. | בדיקה חד-פעמית — לא להריץ שוב |
|
| `run_curator_sonnet_rerun.sh` | A/B test #3 (2026-05-05) — ריצה חוזרת של Sonnet 4.5 על אותו CMP-78. תוצאה: 12:52 דק׳ (לעומת 20:13 בריצה המקורית — כי בלי לולאת interaction.json). זיהה תוצאה שגויה ("דחייה") **בעקביות עם הריצה המקורית** — Sonnet עקבי-בטעות, DeepSeek אקראי. | בדיקה חד-פעמית — לא להריץ שוב |
|
||||||
|
| `ingest_incoming_batch.py` | python | קליטת batch של החלטות ועדת ערר מ-`data/precedents/incoming/` דרך המסלול הקנוני (`ingest_internal_decision`) + חילוץ מטא-דאטה לכל תיק (המסלול הפנימי לא מתזמן metadata — INV-ING3). רצף (לא מקבילי, להימנע מעומס CLI). רשימת `DECISIONS` נערכת ידנית לכל batch. config מ-`~/.env`. תומך תהליך [[project_precedent_incoming_workflow]]. | ידני, per-batch (חלופה ל-MCP `internal_decision_upload` כש-batch גדול) |
|
||||||
|
| `drain_halacha_queue.py` | python | ריקון תור חילוץ ההלכות (`process_pending_extractions kind='halacha'`) ב-batches של 4 עד שהתור ריק (2 סבבים ריקים). משמש אחרי `ingest_incoming_batch.py`. | ידני אחרי batch (חלופה ל-MCP `precedent_process_pending`) |
|
||||||
|
|
||||||
## סקריפטים שנמחקו (git history בלבד)
|
## סקריפטים שנמחקו (git history בלבד)
|
||||||
|
|
||||||
|
|||||||
89
scripts/migrate_gap51_outcomes.py
Normal file
89
scripts/migrate_gap51_outcomes.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""GAP-51 — נרמול ערכי outcome לאוצר-המילים הקנוני (FU-14).
|
||||||
|
|
||||||
|
ממפה את אוצר-המילים הישן של `set_outcome` לקנוני בשתי עמודות:
|
||||||
|
decisions.outcome ו- cases.expected_outcome
|
||||||
|
rejected → rejection
|
||||||
|
accepted → full_acceptance
|
||||||
|
partial → partial_acceptance
|
||||||
|
|
||||||
|
דטרמיניסטי וקטן (~9 שורות). `betterment_levy` אינו קיים בנתונים ואינו ממופה
|
||||||
|
(הוא practice_area, לא outcome). הקוד כבר canonicalize בקריאה, אז המיגרציה היא
|
||||||
|
לניקיון בלבד — לא חוסמת.
|
||||||
|
|
||||||
|
שימוש:
|
||||||
|
python3 scripts/migrate_gap51_outcomes.py # dry-run (ברירת מחדל)
|
||||||
|
python3 scripts/migrate_gap51_outcomes.py --apply # גיבוי ואז עדכון
|
||||||
|
|
||||||
|
דורש POSTGRES_URL / DATABASE_URL בסביבה. נוגע רק ב-cases/decisions — לא בטבלאות
|
||||||
|
החילוץ (case_law/halachot), כך שבטוח להריץ במקביל לחילוץ פעיל.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
MAP = {"rejected": "rejection", "accepted": "full_acceptance", "partial": "partial_acceptance"}
|
||||||
|
TARGETS = [("decisions", "outcome"), ("cases", "expected_outcome")]
|
||||||
|
AUDIT_DIR = Path(__file__).resolve().parent.parent / "data" / "audit"
|
||||||
|
|
||||||
|
|
||||||
|
def _db_url() -> str:
|
||||||
|
url = os.environ.get("POSTGRES_URL") or os.environ.get("DATABASE_URL", "")
|
||||||
|
if not url:
|
||||||
|
raise SystemExit("POSTGRES_URL / DATABASE_URL not set")
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
async def main(apply: bool) -> None:
|
||||||
|
conn = await asyncpg.connect(_db_url())
|
||||||
|
try:
|
||||||
|
affected = []
|
||||||
|
for table, col in TARGETS:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
f"SELECT id, {col} AS val FROM {table} WHERE {col} = ANY($1::text[])",
|
||||||
|
list(MAP.keys()),
|
||||||
|
)
|
||||||
|
for r in rows:
|
||||||
|
affected.append((table, col, str(r["id"]), r["val"], MAP[r["val"]]))
|
||||||
|
|
||||||
|
print(f"GAP-51 outcome migration — {len(affected)} שורות מושפעות:")
|
||||||
|
for t, c, rid, old, new in affected:
|
||||||
|
print(f" {t}.{c} {rid} {old} → {new}")
|
||||||
|
if not affected:
|
||||||
|
print("אין מה לנרמל — כל הערכים כבר קנוניים.")
|
||||||
|
return
|
||||||
|
if not apply:
|
||||||
|
print("\n(dry-run — להחלה הוסף --apply)")
|
||||||
|
return
|
||||||
|
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup = AUDIT_DIR / f"gap51-outcome-backup-{ts}.csv"
|
||||||
|
with backup.open("w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["table", "column", "id", "old_value", "new_value"])
|
||||||
|
w.writerows(affected)
|
||||||
|
print(f"\nגיבוי נכתב: {backup}")
|
||||||
|
|
||||||
|
async with conn.transaction():
|
||||||
|
for table, col in TARGETS:
|
||||||
|
for old, new in MAP.items():
|
||||||
|
await conn.execute(
|
||||||
|
f"UPDATE {table} SET {col} = $1 WHERE {col} = $2", new, old,
|
||||||
|
)
|
||||||
|
print(f"הוחלו {len(affected)} עדכונים בהצלחה.")
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--apply", action="store_true", help="execute (default: dry-run)")
|
||||||
|
asyncio.run(main(ap.parse_args().apply))
|
||||||
@@ -32,12 +32,13 @@ const dateString = z
|
|||||||
message: "תאריך חייב להיות בפורמט YYYY-MM-DD",
|
message: "תאריך חייב להיות בפורמט YYYY-MM-DD",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GAP-51: outcome = 3 canonical values only. "betterment_levy" is a
|
||||||
|
// practice_area (selected in its own field), not an outcome.
|
||||||
export const expectedOutcomes = [
|
export const expectedOutcomes = [
|
||||||
{ value: "", label: "— לא נקבע —" },
|
{ value: "", label: "— לא נקבע —" },
|
||||||
{ value: "rejection", label: "דחייה" },
|
{ value: "rejection", label: "דחייה" },
|
||||||
{ value: "partial_acceptance", label: "קבלה חלקית" },
|
{ value: "partial_acceptance", label: "קבלה חלקית" },
|
||||||
{ value: "full_acceptance", label: "קבלה מלאה" },
|
{ value: "full_acceptance", label: "קבלה מלאה" },
|
||||||
{ value: "betterment_levy", label: "היטל השבחה" },
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/* proceeding_type — distinguishes a regular appeal (ערר) from an
|
/* proceeding_type — distinguishes a regular appeal (ערר) from an
|
||||||
|
|||||||
Reference in New Issue
Block a user