הכרעת-יו"ר: קנוני = 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>
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
#!/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))
|