feat(learning): אינדיקציית-תיק למצב למידת-קול + חילוץ-הלכות אחרי החלטה סופית
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s

אחרי העלאת החלטה סופית והרצת שני הפייפליינים האוטומטיים (למידת-קול,
חילוץ/אימות-הלכות), התיק לא הציג אם כל תהליך בוצע/הצליח/למה-נכשל. במיוחד
תקלת chair_name ריק (2026-06-12) שמפילה בשקט את העתק-ה-case_law → חילוץ-הלכות
לא מתחיל בכלל, בלי שזה גלוי. כעת מוצגות שתי אינדיקציות ליד כפתורי-ההרצה.

Backend (גזירה ממקור-יחיד, ללא מסלול-מעקב מקביל):
- SCHEMA_V36: draft_final_pairs.learning_run (JSONB) — שדה-תיעוד על פנקס-ההתאמה
  (INV-LRN4), חותם את תוצאת-הריצה של פייפליין-הלמידה (succeeded/failed+סיבה+at).
- set_learning_run_outcome() — חיתום הצלחה/כישלון על ה-pair האחרון.
- case_learning_status() — גזירה read-only מ-draft_final_pairs/style_corpus/
  decision_lessons/case_law/halachot: בוצע? הצליח? למה-לא? כמה הלכות חולצו.
- final_learning_pipeline.py — חותם outcome בהצלחה וב-except (surfaced, לא בלוע).
- חשיפה: case_get מוסיף learning_status (→MCP + /api/cases/{case}/details) +
  endpoint ייעודי GET /api/cases/{case}/learning-status (אותה פונקציה — בלי כפילות).

UI (אושר דרך שער-העיצוב Claude Design — כרטיס 21-final-learning-status):
- useCaseLearningStatus (api/learning.ts) — hook + polling עדין בזמן in-flight.
- LearningStatusBadges — 2 שורות (למידת-קול / חילוץ-הלכות) עם badge + תת-שורה
  (מס' לקחים · רישום-קורפוס / מס' הלכות + פירוק אושרו/ממתינות/נדחו / סיבת-כישלון).
- שילוב ב-drafts-panel תחת "החלטה סופית של היו״ר" + אינוולידציה בכפתורי-ההרצה.

אומת מול ה-DB החי: הצליח+5 הלכות (8174-12-24) · נכנס-אך-pending (1200-12-25) ·
לא-נכנס-לקורפוס (8125-09-24) · round-trip חיתום-כישלון. tsc/eslint נקיים.

Invariants: G1 (נרמול-במקור — גזירה, לא טלאי), G2 (אין מסלול מקביל — שדה על
הפנקס הקיים + exposer יחיד), INV-LRN4 (פנקס-ההתאמה), INV-IA1 (מקור-אמת יחיד).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 10:50:12 +00:00
parent 584bc62488
commit 959cb093b4
7 changed files with 461 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@@ -34,6 +35,8 @@ import {
type FeedbackCategory,
} from "@/lib/api/feedback";
import { useCaseCitations } from "@/lib/api/citations";
import { learningKeys } from "@/lib/api/learning";
import { LearningStatusBadges } from "@/components/cases/learning-status-badges";
import type { CaseStatus } from "@/lib/api/cases";
import { toast } from "sonner";
import {
@@ -98,6 +101,7 @@ export function DraftsPanel({
const uploadFinal = useUploadFinalDecision(caseNumber);
const runLearning = useRunFinalLearning(caseNumber);
const runHalacha = useRunFinalHalacha(caseNumber);
const qc = useQueryClient();
const fileRef = useRef<HTMLInputElement>(null);
const finalFileRef = useRef<HTMLInputElement>(null);
@@ -186,20 +190,29 @@ export function DraftsPanel({
function handleRunLearning() {
runLearning.mutate(undefined, {
onSuccess: (d) =>
d.status === "ok"
? toast.success("למידת-הקול הופעלה — רצה ברקע (אופוס + פאנל דיפסיק/גמיני)")
: toast.warning(`לא הופעלה למידה: ${d.reason ?? d.error ?? d.status}`),
onSuccess: (d) => {
// Background run — refresh the indicator (the poll picks up later transitions).
qc.invalidateQueries({ queryKey: learningKeys.caseStatus(caseNumber) });
if (d.status === "ok") {
toast.success("למידת-הקול הופעלה — רצה ברקע (אופוס + פאנל דיפסיק/גמיני)");
} else {
toast.warning(`לא הופעלה למידה: ${d.reason ?? d.error ?? d.status}`);
}
},
onError: () => toast.error("שגיאה בהפעלת למידת-הקול"),
});
}
function handleRunHalacha() {
runHalacha.mutate(undefined, {
onSuccess: (d) =>
d.status === "ok"
? toast.success("אימות-ההלכות הופעל — רץ ברקע (פאנל אופוס/דיפסיק/גמיני)")
: toast.warning(`לא הופעל אימות: ${d.reason ?? d.error ?? d.status}`),
onSuccess: (d) => {
qc.invalidateQueries({ queryKey: learningKeys.caseStatus(caseNumber) });
if (d.status === "ok") {
toast.success("אימות-ההלכות הופעל — רץ ברקע (פאנל אופוס/דיפסיק/גמיני)");
} else {
toast.warning(`לא הופעל אימות: ${d.reason ?? d.error ?? d.status}`);
}
},
onError: () => toast.error("שגיאה בהפעלת אימות-ההלכות"),
});
}
@@ -337,6 +350,7 @@ export function DraftsPanel({
</span>
</div>
)}
{hasFinal && <LearningStatusBadges caseNumber={caseNumber} />}
</section>
{/* ── Precedents cited inside the signed decision ── */}