refactor(cases): צמצום תפריט-סטטוס 17→10 + מקור-אמת יחיד (UI-B1/G2)
תפריט הסטטוס-הידני הכיל 17 סטטוסים שמתוכם ~9 דקורציה טהורה — שלבי-ביניים שאף קוד בפייפליין לא קבע ושום לוגיקה לא הסתעפה לפיהם, עם רשימות כפולות לא-עקביות ב-6+ קבצים (UI-B1) ו-exported כסטטוס-רפאים (באג agent-audit). הליבה (10): new, processing, documents_ready, outcome_set, direction_approved, qa_review, drafted, exported, reviewed, final. - SSoT חדש web-ui/src/lib/api/case-status.ts (רשימה/שלבים/תוויות/statusLabel); כל הצרכנים (badge/changer/timeline/guide/donut/kpi/compose) מייבאים משם. - statusLabel() מבטיח תווית עברית תמיד — גם לערך-מורשת (נפילה עברית, לא סלאג). - בקאנד: STATUS_ORDER 10, models.CaseStatus מיושר, set_outcome קובע outcome_set/direction_approved (במקום in_progress) כמו endpoint ה-web. - exported מוקשח אחרי export-DOCX מוצלח (forward-only); widget "נכשל ב-QA" עודכן ל-qa_review (הסטטוס שנקבע בפועל בכשל-QA). - scripts/backfill_case_status_trim.py: מיפוי שורות-מורשת לסטטוס-הליבה הקודם. Invariants: UI-B1 (מקור-אמת יחיד) ✅ · G2 (אין מסלול מקביל) ✅ · GAP-42 (חלקי). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,7 @@
|
||||
| `fu2c_reconcile_external_case_numbers.py` | python | **FU-2c (GAP-08, #68) — תיאום `case_number` של פסיקה חיצונית** (`source_kind <> internal_committee`) מציטוט-מלא לצורה קנונית **מציין-הליך + docket** (החלטת-יו"ר 2026-05-31, Option A: `/` נשמר, *לא* `-`; תואם db.py:369 ו-INV-ID2). דטרמיניסטי (designator+docket; 0/>1 docket → flag). `--dry-run` (ברירת-מחדל) מפיק `data/audit/fu2c-reconciliation-*.{csv,md}` עם flags (MISMATCH / NO_CITATION / CIT_NO_DOCKET / DESIG_MISMATCH / DUP_CHECK). `--apply --approved <csv>` מגבה ואז מעדכן שורות לא-חוסמות (כולל ADVISORY/NO_CITATION). `--overrides <csv>` (id,proposed_canonical,reason) פותח שורות-חוסמות בהכרעת-יו"ר מפורשת (למשל פס"ד מאוחד — ראה `data/audit/fu2c-overrides.csv` לרשומת לויתן/קלמנוביץ). לוגיקת-החילוץ + פיצול flags אומתו offline על 24 רשומות. scope: external בלבד (internal = FU-2b). FK-safe. | חד-פעמי, **chair-gated** (apply רק אחרי אישור דפנה) |
|
||||
| `fix_137_committee_case_number.py` | python | **#137 — תיקון-נתון חד-פעמי**: רשומת `internal_committee` בודדת (1bf0bae0) שבה ציטוט-מלא זיהם את שדה-המזהה (case_number=`85074/0425`, case_name=ציטוט שלם) — הפרת INV-ID2 ממסלול `missing_precedent_upload` (לפני תיקון-הקוד ב-#137). מתקן `case_number`→`85074-04-25`, `case_name`→צדדים, ו-token ב-`citation_formatted`. אומת היחיד עם `_canonical_case_number(num)≠num` ב-internal_committee (138 ה"מזוהמים" האחרים = מקור-חיצוני/cited_only מקודמים-קידומת, X1 §5 — מחוץ-לתחום). `document_id=NULL`, 0 ציטוטים-נכנסים → ללא נתיב/קובץ לשנות. guard-התנגשות על `(case_number,proceeding_type)`. אידמפוטנטי, dry-run כברירת-מחדל / `--apply`. הרצה: `HOME=/home/chaim PYTHONPATH=mcp-server/src mcp-server/.venv/bin/python scripts/fix_137_committee_case_number.py --apply`. | חד-פעמי (בוצע 2026-06-15) |
|
||||
| `retrofit_case.py` | python | retrofit רטרואקטיבי — מזריק bookmarks לקובץ קיים של תיק ספציפי ומגדיר אותו כ-active_draft | ידני (חד-פעמי לתיק) |
|
||||
| `backfill_case_status_trim.py` | python | **צמצום תפריט-הסטטוס 17→10** — ממפה כל `cases.status` שהוסר (uploading/in_progress/analyst_verified/research_complete/brainstorming/analysis_enriched/ready_for_writing/drafting/qa_failed) לסטטוס-הליבה הקודם-ביותר ברצף (→processing/outcome_set/documents_ready/direction_approved/qa_review), כך ששורות קיימות לא נתקעות על ערך מחוץ ל-SSoT. מציג קודם את פילוח-הסטטוסים המלא. אידמפוטנטי, dry-run כברירת-מחדל / `--apply`. רץ בקונטיינר (Postgres :5433 משותף): `docker exec <c> python /tmp/backfill_case_status_trim.py --apply`. | חד-פעמי (אחרי deploy של הצמצום) |
|
||||
|
||||
### פסיקה, קורפוס ויומונים
|
||||
|
||||
|
||||
115
scripts/backfill_case_status_trim.py
Normal file
115
scripts/backfill_case_status_trim.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Backfill case.status after the 17 → 10 status-menu trim.
|
||||
|
||||
Why this exists: the manual status menu was trimmed from 17 to 10 core
|
||||
statuses (decorative mid-stage markers that no pipeline code ever set were
|
||||
removed). Existing rows that currently hold a removed status would otherwise
|
||||
be "stuck" on a value no longer in the dropdown / SSoT, rendering via the
|
||||
Hebrew legacy fallback. This maps each removed status to the nearest
|
||||
*preceding* kept status in the lifecycle order, so a case keeps the closest
|
||||
truthful position.
|
||||
|
||||
Mapping (removed → kept):
|
||||
|
||||
uploading → processing
|
||||
in_progress → outcome_set
|
||||
analyst_verified → documents_ready
|
||||
research_complete → documents_ready
|
||||
brainstorming → outcome_set
|
||||
analysis_enriched → direction_approved
|
||||
ready_for_writing → direction_approved
|
||||
drafting → direction_approved
|
||||
qa_failed → qa_review
|
||||
|
||||
Idempotent: a second run is a no-op (no rows match the removed statuses).
|
||||
Dry-run by default — prints the affected counts; pass --apply to write.
|
||||
|
||||
Usage (runs inside the legal-ai container — shared Postgres on :5433):
|
||||
docker cp scripts/backfill_case_status_trim.py <c>:/tmp/
|
||||
docker exec <c> python /tmp/backfill_case_status_trim.py # dry-run
|
||||
docker exec <c> python /tmp/backfill_case_status_trim.py --apply # write
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _setup_paths():
|
||||
here = Path(__file__).resolve().parent
|
||||
mcp_src = here.parent / "mcp-server" / "src"
|
||||
if mcp_src.is_dir() and str(mcp_src) not in sys.path:
|
||||
sys.path.insert(0, str(mcp_src))
|
||||
|
||||
|
||||
_setup_paths()
|
||||
from legal_mcp.services import db # noqa: E402
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
log = logging.getLogger("status-trim")
|
||||
|
||||
# removed status → nearest preceding kept status
|
||||
STATUS_MAP = {
|
||||
"uploading": "processing",
|
||||
"in_progress": "outcome_set",
|
||||
"analyst_verified": "documents_ready",
|
||||
"research_complete": "documents_ready",
|
||||
"brainstorming": "outcome_set",
|
||||
"analysis_enriched": "direction_approved",
|
||||
"ready_for_writing": "direction_approved",
|
||||
"drafting": "direction_approved",
|
||||
"qa_failed": "qa_review",
|
||||
}
|
||||
|
||||
|
||||
async def backfill(apply: bool) -> int:
|
||||
pool = await db.get_pool()
|
||||
|
||||
# Show the full current distribution for context.
|
||||
dist = await pool.fetch("SELECT status, count(*) AS n FROM cases GROUP BY status ORDER BY n DESC")
|
||||
log.info("Current status distribution:")
|
||||
for r in dist:
|
||||
log.info(" %-22s %d", r["status"], r["n"])
|
||||
|
||||
affected = {r["status"]: r["n"] for r in dist if r["status"] in STATUS_MAP}
|
||||
total = sum(affected.values())
|
||||
if not total:
|
||||
log.info("Nothing to migrate — no rows hold a removed status. ✓")
|
||||
return 0
|
||||
|
||||
log.info("Rows to migrate (%d total):", total)
|
||||
for old, n in affected.items():
|
||||
log.info(" %-22s → %-20s (%d)", old, STATUS_MAP[old], n)
|
||||
|
||||
if not apply:
|
||||
log.info("DRY-RUN — no changes written. Re-run with --apply to migrate.")
|
||||
return total
|
||||
|
||||
migrated = 0
|
||||
for old, new in STATUS_MAP.items():
|
||||
if old not in affected:
|
||||
continue
|
||||
res = await pool.execute(
|
||||
"UPDATE cases SET status = $1, updated_at = now() WHERE status = $2",
|
||||
new, old,
|
||||
)
|
||||
# res like "UPDATE 3"
|
||||
n = int(res.split()[-1]) if res and res.split()[-1].isdigit() else 0
|
||||
migrated += n
|
||||
log.info(" migrated %-22s → %-20s (%d)", old, new, n)
|
||||
|
||||
log.info("Done — migrated %d rows.", migrated)
|
||||
return migrated
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Backfill case.status after the 17→10 status trim")
|
||||
parser.add_argument("--apply", action="store_true", help="Write changes (default: dry-run)")
|
||||
args = parser.parse_args()
|
||||
return 0 if asyncio.run(backfill(args.apply)) >= 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user