feat(mcp): FU-14 GAP-51 — איחוד אוצר-המילים של תוצאת-תיק (set_outcome SSoT)

הכרעת-יו"ר: קנוני = 3 תוצאות אמיתיות (rejection/partial_acceptance/full_acceptance);
betterment_levy יוצא מהיותו "תוצאה" ועובר ל-override לפי practice_area.
+ עקרון "אנגלית-ב-DB, עברית-ב-UI": מפת-תוויות SSoT אחת.

lessons.py:
- VALID_OUTCOMES = 3 (הוסר betterment_levy).
- OUTCOME_LABELS_HE (SSoT לתצוגה) + LEGACY_OUTCOME_MAP + canonical_outcome().
- PRACTICE_AREA_OVERRIDES["betterment_levy"] מרכז את כל ה-guidance שהיה מפתוח כ-outcome
  (golden_ratios/opening/summary/discussion/template).
- get_lessons_for_outcome(outcome, practice_area) + format_ratios_comment(..., practice_area)
  מחילים override + מנרמלים legacy.

block_writer.py: STRUCTURE_GUIDANCE קנוני + תווית מ-OUTCOME_LABELS_HE + override betterment.
workflow.set_outcome: קנוני 3 + מיפוי-legacy סלחני; תווית מ-SSoT.
drafting.py: טבלת יחסי-זהב + get_decision_template מודעי-practice_area (override).
web-ui case.ts: הסרת betterment_levy מ-expectedOutcomes (הוא practice_area).
server.py: docstrings קנוניים.

מיגרציה: migrate_gap51_outcomes.py — 9 שורות נורמלו (rejected→rejection וכו'),
גיבוי ב-data/audit/. הקוד canonicalize בקריאה ⇒ backward-compatible גם בלי מיגרציה.

אומת: py_compile (5 קבצים) + בדיקות-יחידה offline (override/legacy/labels) + אימות-DB.
עודכנו X9 §3 + gap-audit (GAP-51 ).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:34:49 +00:00
parent d3f1d04915
commit 701efab726
10 changed files with 267 additions and 73 deletions

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