From 94a4c3600e648ef725a3f96920474485578c2527 Mon Sep 17 00:00:00 2001 From: Chaim Date: Thu, 11 Jun 2026 17:07:41 +0000 Subject: [PATCH] =?UTF-8?q?fix(learning):=20process=5Ffinal=5Fversion=20?= =?UTF-8?q?=D7=9E=D7=90=D7=97=D7=A1=D7=9F=20=D7=93=D7=99=D7=A1=D7=98=D7=99?= =?UTF-8?q?=D7=9C=D7=A6=D7=99=D7=94=20=D7=92=D7=9D=20=D7=9B=D7=A9=D7=90?= =?UTF-8?q?=D7=99=D7=9F=20pair=20(create-or-update,=20INV-LRN4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit אודיט #122 חשף ש-process_final_version מחשב diff+analysis אך משליך אותם כשאין draft_final_pair במצב final_received — קרה ל-5 תיקים סופיים היסטוריים שקדמו למנגנון ה-snapshot ב-mark-final (pair ראשון 2026-06-06), ולכל קריאת ingest_final_version ישירה. התוצאה: הפרת INV-LRN4 בפועל (סופי שלא הושווה/נשמר). התיקון: create-or-update — כשאין pair, פותחים אחד מ-decision_blocks החיים (status→analyzed) כך שהדיסטילציה נשמרת כ-הצעה ברשם. לתיקים חדשים אין שינוי-התנהגות (תמיד יש pair מ-mark-final → רק ה-update רץ). זה keystone שמאפשר backfill (#125.2) דרך הפייפליין הקיים. caveat מתועד בלוג: לתיק היסטורי ה-draft = blocks נוכחיים (אולי נערכו אחרי-חתימה), לא snapshot-אמיתי. Invariants: - INV-LRN4 (מקיים) — כל סופי מקבל pair ומנותח; אין סופי "פתוח". - INV-LRN1/G10 (נשמר) — הדיסטילציה נשמרת כ-הצעה (analyzed) בלבד; שער ה-promote הידני לקיפול ל-appeal_type_rules לא נעקף. - G2 (מקיים) — אותו פנקס draft_final_pairs, לא מסלול מקביל. - G1 (מקיים) — נרמול במקור (הרשם) במקום תיקון-בקריאה. ref: data/audit/learning-loop-activity-20260611.md · TaskMaster legal-ai #122/#125.1 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/legal_mcp/services/learning_loop.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/learning_loop.py b/mcp-server/src/legal_mcp/services/learning_loop.py index 14fa9a7..1b55217 100644 --- a/mcp-server/src/legal_mcp/services/learning_loop.py +++ b/mcp-server/src/legal_mcp/services/learning_loop.py @@ -149,14 +149,28 @@ async def process_final_version( # it (→ decision_lessons / appeal_type_rules, surfaced by T15) via the gate. # (Previously this auto-upserted every new_expression as a style_pattern — # that both bypassed the gate and contaminated style with substance. Removed.) - if pair_id is not None: - await db.update_draft_final_pair( - UUID(str(pair_id)), - final_text=final_text, - diff_stats=diff_stats, - analysis=analysis, - status="analyzed", + # + # create-or-update (INV-LRN4): normally mark-final already opened a + # 'final_received' pair, so we just advance it. For a case whose final + # pre-dates the mark-final snapshot mechanism (historical backfill) or a direct + # ingest_final_version call, no pair exists — open one now from the live blocks + # so the distillation is actually persisted instead of silently discarded. + # Caveat: the captured draft is the CURRENT blocks (possibly edited after + # sign-off), not a true mark-final snapshot. + if pair_id is None: + pair_id = await db.create_draft_final_pair(case_id, draft_text, "") + logger.info( + "process_final_version: no 'final_received' pair for case %s — opened one " + "from live blocks (backfill path; draft may post-date sign-off)", + case_id, ) + await db.update_draft_final_pair( + UUID(str(pair_id)), + final_text=final_text, + diff_stats=diff_stats, + analysis=analysis, + status="analyzed", + ) # Update decision + case status await db.update_decision(UUID(decision["id"]), status="final")