#!/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))