fix(precedents): חילוץ-מטא-דאטה ממלא תחום (practice_area) ושם-יו"ר לכל החלטת-ועדה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

שני פערים שצפו מ-/precedents בחילוץ-ההלכות:

1. **practice_area לא סומן** — השדה הועבר ל-LLM כקונטקסט-קריאה-בלבד ולא חולץ
   מעולם, כך שהעלאות שהשאירו אותו ריק נשארו ריקות והרדיו ב-/precedents הופיע
   ללא בחירה. עכשיו נגזר ב-apply_to_record: עדיפות לגזירה דטרמיניסטית מקידומת
   מספר-התיק (1xxx→rishuy, 8xxx→היטל, 9xxx→197 — מקור-אמת לדוקטי ועדת-ערר,
   INV-AH rule-based), ובנפילה — סיווג-תוכן של ה-LLM (שדה practice_area חדש
   בפרומפט, אנום-סגור) עבור פסקי-בית-משפט שהקידומת שלהם אינה מקודדת תחום.
   ממלא רק כשריק (G1 — נרמול במקור, לא תיקון-בקריאה).

2. **שם-יו"ר לא חולץ** (למשל 1132-09-24) — המיזוג היה מגודר על
   source_kind=='internal_committee' בלבד, ודילג בשקט על החלטות-ועדה שהועלו
   במסלול הפסיקה החיצוני (external_upload + source_type=appeals_committee, כמו
   החלטת ת"א מנבו) — היו"ר ישב בבלוק-החתימה אך לא חולץ. עכשיו מגודר על "האם זו
   החלטת-ועדה" (source_type/level אפקטיביים), לעולם לא על פסק-בית-משפט. ה-CHECK
   כופה non-empty רק ל-internal_committee, לכן כתיבה ל-external בטוחה.

חיזוק-פרומפט (לבקשת היו"ר): chair_name מציין מפורשות את בלוק-החתימה הדו-טורי
(מזכיר↔יו"ר — לקחת את צד-היו"ר) ומזהיר לא לחלץ יו"ר של פסקי-דין **מצוטטים**
בגוף ההחלטה.

UI (לוגיקה-בלבד, פטור משער-העיצוב): edit-sheet מסנכרן-מחדש מהרשומה הטרייה בכל
פתיחה (re-arm על סגירה) ו-usePrecedent עושה poll בזמן חילוץ — כך מילוי-רקע של
practice_area/chair_name מופיע בלי refresh מלא ("הכפתור לא נשאר מסומן").

בדיקות: test_metadata_extract_chair_practice_area.py (6 תרחישי-מיזוג, offline).

Invariants: G1 (נרמול-במקור), G2 (אותו extractor, לא מסלול מקביל),
INV-AH (גזירה דטרמיניסטית מועדפת, abstention כשאין ודאות).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 09:53:42 +00:00
parent 5370ada37c
commit 406e93b9bf
4 changed files with 245 additions and 7 deletions

View File

@@ -73,7 +73,18 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
// record arrives (including after save+refetch). Using setState during
// render avoids the one-frame flash that useEffect would produce.
const [syncedRecordId, setSyncedRecordId] = useState<string | null>(null);
if (record && record.id !== syncedRecordId) {
// Re-arm the sync on close so the NEXT open re-pulls the latest server record.
// The component is always mounted, so without this the form syncs once per
// precedent-id for the component's lifetime — and shows stale fields (e.g. a
// practice_area / chair_name that background metadata extraction filled AFTER
// the last open) until a full page refresh. Resetting on close makes reopening
// the sheet reflect the freshest record (which usePrecedent re-fetches while a
// row is mid-extraction). Both guards flip to false, so this render-phase
// setState terminates.
if (!open && syncedRecordId !== null) {
setSyncedRecordId(null);
}
if (open && record && record.id !== syncedRecordId) {
setSyncedRecordId(record.id as string);
setForm({
case_number: record.case_number || "",

View File

@@ -295,6 +295,15 @@ export function usePrecedent(id: string | null) {
),
enabled: Boolean(id),
staleTime: 30_000,
/* Poll while THIS precedent is mid-extraction (text/halacha/metadata) so the
* detail catches up after the local MCP drainer fills fields like
* practice_area / chair_name — otherwise the open edit sheet shows stale
* empties until a hard refresh. Stops once the row settles (mirrors the
* list poller in usePrecedents). */
refetchInterval: (query) => {
const data = query.state.data;
return data && isPrecedentActive(data) ? 5000 : false;
},
});
}