ה-backfill של citation_formatted חשף קריסה ב-apply_to_record: כשפסק-דין
חיצוני מכיל docket שכבר שייך לרשומה כפולה אחרת, נרמול case_number → docket-נקי
נתקל ב-uq_case_law_external_number ומפיל את כל המיזוג (כולל הציטוט).
דוגמה: 'ע"א 3213/97' → '3213/97' שכבר קיים (כפילות נקר).
- db.case_number_collides(case_number, exclude_id) — בודק אם docket כבר שייך
לרשומה לא-internal אחרת (האינדקס החלקי).
- apply_to_record — מדלג על נרמול ה-case_number כשיש התנגשות (כפילות לדדופ
בהמשך, לא ענייננו כאן) וממשיך לכתוב את הציטוט. no-silent-swallow: מתעד warning.
- scripts/backfill_precedent_citations.py — try/except per-row + מונה שגיאות,
כך ששורה אחת לא מפילה את האצווה.
אומת: ריצה-מחדש מלאה ללא קריסה (0 שגיאות); ההתנגשות תועדה ודולגה כצפוי;
פסיקת בית-משפט: 224/228 מולאו, 4 נמנעו (חסר צדדים/תאריך — abstention, INV-AH).
test_fu2b_reconcile ✓.
Invariants: INV-AH (abstention) · G1 (נרמול-בכתיבה נשמר, רק לא קורס) ·
חוקה §6 (אין בליעה שקטה — דילוג מתועד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תיק יכול להיות <kind>_extraction_status='pending' עם _requested_at=NULL —
מעולם-לא-נכנס-לתור (מסלולי bulk/מיגרציה, או status שנכתב לפני החותם), והדריינר
(סורק requested_at IS NOT NULL) עיוור אליו לנצח → ה-backlog מתנקז בשקט לאפס.
requeue_stale_processing_extractions מרפא רק 'processing'. נצפה 2026-06-14:
96 תיקים pending אך 0 בתור (תוקנו ידנית).
תיקון (G1 — שחזור invariant במקור, G2 — predicate יחיד):
- db.reconcile_orphaned_pending_extractions(kind=) — kind-agnostic, מחזיר את
invariant "שורה ברת-חילוץ ⇒ בתור": חותם requested_at ל-rows שהם pending +
requested_at IS NULL + EXTRACTION_ELIGIBLE_PREDICATE (אותו מסנן של #140 —
cited_only/chunkless לעולם לא נדחפים). אידמפוטנטי (rows מסומנים לא נתפסים).
- precedent_library.process_pending_extractions קורא reconcile אחרי requeue_stale
ולפני list — תיקים-משוחזרים נקלטים באותו pass. מנגנון-ריפוי יחיד (G2), לא מסלול
מקביל; requeue_stale='processing', reconcile='pending'.
- request_halacha_extraction מציב status='pending' עם החותם (סימטרי ל-metadata)
— סוגר את חלון-ה-drift שמייצר pending+NULL מלכתחילה.
מצב חי נקי (0 יתומים-כשירים אחרי התיקון-הידני); זהו תיקון מונע — הדריינר יְרַפֵּא
יתומים עתידיים אוטומטית.
בדיקות: test_extraction_orphan_reconcile (predicate משותף, pending+NULL בלבד,
מובחן מ-requeue_stale, request_halacha סימטרי), שני ה-kinds. כל 349 עוברות.
Invariants: G1, G2 (predicate משותף עם #140, ריפוי יחיד), INV-G3/INV-DUR1 (X16),
INV-G4 (אין בליעה שקטה — reconcile מתעד), G12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 שורות case_law עם source_kind='cited_only' (ציטוט-בלבד, ללא full_text/chunks)
נושאות halacha_extraction_status='pending' רק כברירת-מחדל ומזהמות את מונה ה-pending
ובמתזמר/בדף-התפעול — אין להן מה לחלץ.
תיקון (G1 — תיקון-במקור, G2 — מסנן יחיד משותף):
- db.EXTRACTION_ELIGIBLE_PREDICATE — מקור-אמת יחיד ל"שורה ברת-חילוץ" (source_kind
<> 'cited_only' AND יש precedent_chunks). מוחל ב-list_pending_extraction_requests;
#139 יעשה בו שימוש-חוזר ל-reconcile (אותו כלל, לא כפול).
- מוני-snapshot מסננים cited_only: halacha_drain_supervisor.db_snapshot,
web/app.py meta+hal_ext (GROUP BY status).
- reconcile_metadata_status.py מורחב לכסות גם את תור-ההלכות: cited_only→'skipped'
(אותו terminal-state כמו צד-המטא, תור-תאום, G2). בוצע על ה-DB החי: 31 הועברו
ל-'skipped' (metadata כבר היה מיושב — אידמפוטנטי). התפלגות-אחרי: halacha
pending=9 (עבודה אמיתית), skipped=31, completed=309.
בדיקות: test_extraction_queue_eligibility (predicate + list_pending מחיל אותו,
שני ה-kinds). כל 345 בדיקות mcp עוברות. guards נקיים.
Invariants: G1 (terminal-state אמיתי במקור), G2 (predicate יחיד, ללא תור מקביל),
INV-DM1 (stub לא-searchable אינו מועמד-חילוץ), G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
בהעלאה דרך "פסיקה-חסרה" (ענף ועדת-ערר), כשטופס case_number ריק המסלול נפל-לאחור
לציטוט המלא (committee_case_number = case_number.strip() or citation), כך שמחרוזת-
תצוגה עם שמות-צדדים הושתלה בשדה-המזהה — הפרת INV-ID2/INV-ID1 (X1). נצפה על
precedent 1bf0bae0 (ערר 85074-04-25 רפאל לוי/חולון): case_number=85074/0425,
case_name=ציטוט שלם.
תיקון (G1 — נרמול-במקור, G2 — שימוש-חוזר בפרסר הקנוני):
- court_citation.case_number_from_citation(citation) — מחזיר את אסימון-המספר
המנורמל בלבד (classify; '' כשאין מספר). חולץ נכון 85074-04-25 גם מתוך
"ערר (ת\"א 85074-04-25) ...". reuse של הפרסר היחיד, בלי regex מקביל.
- web/app.py (ענף ועדת-ערר): fallback דרך case_number_from_citation; אם אין
מספר — HTTPException 400 "נא להזין מספר-תיק ידנית" במקום השתלת ציטוט-מלא.
- db._canonical_case_number: מוקשח לחלץ את אסימון-המספר (זורק זנב שמות-צדדים),
כך ששדה-המזהה לעולם לא נשמר מזוהם — גם בקריאה ישירה (committee + active cases).
מספר נקי חוזר ללא שינוי; חודש לא מומצא (X1 §1).
- תיקון-נתון: scripts/fix_137_committee_case_number.py (בוצע) — 1bf0bae0:
case_number→85074-04-25, case_name→צדדים, token ב-citation_formatted.
אומת היחיד עם canon(num)≠num ב-internal_committee. אידמפוטנטי.
מחוץ-לתחום (תועד כ-follow-up): מסלול external (precedent_library) משתמש בציטוט-
מלא כמזהה-מורשת — זהו פריט-המיגרציה X1 §5 (138 רשומות external/cited_only),
לא הבאג הזה. prefill ב-UI של /missing-precedents — דורש שער Claude Design.
בדיקות: test_court_citation (case_number_from_citation: party-strip/forms/empty),
test_canonical_case_number (harden). כל 339 בדיקות mcp עוברות. guards נקיים.
Invariants: G1 (נרמול-במקור), INV-ID1/ID2 (מזהה מנורמל, אין ציטוט-מלא כמזהה),
G2 (פרסר יחיד), G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prevents recurrence of the case-rename 500 (PR #249), whose root cause was
an undefined name (`paperclip_client`) sitting in a background_tasks callable
— invisible until that code path ran in production.
- scripts/check_undefined_names.py: runs pyflakes on web/, mcp-server/src,
scripts/ and fails ONLY on "undefined name" / "may be undefined" (the
runtime-crash class). Unused imports / f-strings are NOT gated — keeps the
check high-signal and green.
- .gitea/workflows/lint.yaml: runs the guard on every PR and push to main,
in a throwaway venv (PEP-668 safe).
- db.py: `from datetime import date` → `date, datetime`. The guard surfaced a
real latent undefined name — `insert_panel_round`'s `round_ts: datetime`
annotation referenced an unimported `datetime` (benign only because of
`from __future__ import annotations`; now correct).
- SCRIPTS.md: documented the new guard.
Verified: clean tree → exit 0; injected undefined name → exit 1.
Invariants: engineering rule §6 (no silent failures shipping to runtime).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
legal-halacha-drain crashed 29× with asyncpg DeadlockDetectedError. Root cause:
every short-lived cron drain re-runs the idempotent schema migrations on startup
(get_pool → _run_schema_migrations), and three jobs (metadata-drain, halacha-drain,
halacha-supervisor) all fired on the same minute (*/15 / top-of-hour). Two
processes running the DDL concurrently took AccessExclusiveLock in opposite order
→ Postgres killed one with a deadlock.
Two-layer fix:
- Root cause: wrap _run_schema_migrations in a session-level pg_advisory_lock so
only one process applies DDL at a time; concurrent migrators wait instead of
deadlocking. DDL body extracted to _apply_schema_ddl. Idempotent, schema
unchanged.
- Defence-in-depth: give each cron drain a distinct firing minute —
metadata :00, supervisor :05, halacha-drain :10, digest :12, court-fetch :17 —
so siblings no longer start at the same instant. SCRIPTS.md updated to match.
Invariants: G1 (fix at source — the single migration path — not the symptom);
G2 (no parallel control path introduced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The halacha-extraction backlog needs to be worked off the chair's leftover weekly
Claude quota on demand. This adds a MANUAL, time-boxed "burst" — run the drain
continuously now until a chosen deadline (default the upcoming Saturday 18:00 IL),
managed interactively from /operations — plus the permanent health-supervisor that
enforces it.
Backend (this PR; deploys via Coolify + host pm2):
- db: drain_controls.burst_until (SCHEMA_V37) + set_drain_burst/get_drain_burst/
get_drain_bursts. Single source of truth shared by the container-side /operations
API and the host-side supervisor.
- web: POST /api/operations/drains/{name}/burst (on→until|next-Sat-18:00, off→NULL),
and burst_until surfaced per-service in the /operations snapshot.
- scripts/halacha_drain_supervisor.py + legal-halacha-supervisor.config.cjs: pm2 cron
(*/15, zero Claude quota) — re-triggers idle drain, restarts a HUNG run (liveness =
per-chunk checkpoints, NOT log mtime), backs off on 429 until the parsed reset
(fresh-gated), verifies crash-safe staging. Reads burst_until from the DB; burst
auto-expires at the deadline (never bleeds into a fresh week).
UI (separate follow-up PR, after Claude Design approval): the /operations toggle +
date-picker that calls the burst endpoint.
Invariants: G1 (normalize at source — burst lives once in the DB, read by both
surfaces), G2 (no parallel control path — CAPTURE field on the existing
drain_controls + orchestrates the existing drain, not a new one), G12 (no Paperclip
touch), §6 (no silent error-swallow — burst-clear failure is surfaced as a note).
אחרי העלאת החלטה סופית והרצת שני הפייפליינים האוטומטיים (למידת-קול,
חילוץ/אימות-הלכות), התיק לא הציג אם כל תהליך בוצע/הצליח/למה-נכשל. במיוחד
תקלת 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>
הבעיה: בדף /missing-precedents לא ניתן היה לאתר פסיקה חסרה לפי מספר ההחלטה
החסרה עצמה (למשל 85074). השדה היחיד לחיפוש-תיק עשה get_case_by_number על
מספר ה-ערר שבו צוטטה הפסיקה — ולכן הקלדת מספר-הפסיקה החזירה רשימה ריקה,
למרות שהרשומה קיימת (ערר (ת"א 85074-04-25) ... status=open).
התיקון (הרחבת השדה הקיים, ללא עמוד/שדה חדש — בהנחיית חיים):
- db.list_missing_precedents: פרמטר q חדש — ILIKE על mp.citation +
mp.case_name + cited-in c.case_number (אינדקס-פרמטר יחיד, additive;
שאר הקוראים לא נוגעים).
- GET /api/missing-precedents: פרמטר q; case_id/case_number נשארים
מסננים-מדויקים לקוראים תכנותיים.
- web-ui: התווית "תיק (מספר ערר)" → "מספר תיק", placeholder
"85074 או 1017-03-26"; השדה שולח q (חיפוש חופשי) במקום case_number.
Debounce 350ms נשמר.
api:types לא חודש: ה-hook בונה את ה-querystring ידנית וה-response לא
השתנה; חידוש מול prod (שעוד לא נפרס) רק היה מושך drift לא-קשור.
בדיקות: tsc --noEmit נקי, eslint נקי על הקבצים שהשתנו, py_compile נקי.
Invariants: G2 (הרחבת היכולת הקיימת, לא מסלול-חיפוש מקביל), INV-IA1
(שער/דף יחיד לפסיקה-חסרה — בלי עמוד חדש), §6 (ללא בליעת-שגיאות).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תור-אישור-ההלכות הקיים (order_by_priority, #84.3) מקדם עכשיו את ההלכות
שהפאנל התלבט עליהן: split קודם, אחר-כך incomplete — התוויות בעלות-הערך
הגבוה ביותר ללולאת-הלמידה (הכרעת-היו"ר מפרקת אי-ודאות אמיתית ומזינה את
זיקוק-ה-rubric ב-FU-4). uncertainty-sampling על סיגנל-המחלוקת האמיתי של
הפאנל, לא רק confidence-החילוץ.
- list_halachot: LEFT JOIN לאחרון-הסבבים (DISTINCT ON latest round_ts מ-
halacha_panel_rounds) + מפתח-מיון ראשי CASE verdict split→0/incomplete→1/
else→2, לפני מפתחות #84.3 (corroboration→confidence→age). סבבים פה-אחד
ופריטים-ללא-סבב נשארים בזנב עם הסדר הקיים.
- panel_verdict נחשף בכל שורה (UI יכול לתייג "פיצול" + ביקורת-סדר).
- שימוש חוזר בדגל order_by_priority הקיים ובטאב הקיים — בלי מסלול/דגל
מקביל (G2). ה-UI כבר מבקש order_by_priority=true → אפס שינוי-UI, אין
צורך בשער-עיצוב.
- test_halacha_priority_panel_order.py: 3 בדיקות offline (SQL-capture) —
מפתח-מחלוקת ראשי בעדיפות, FIFO ללא דליפת-CASE, panel_verdict נבחר.
Invariants: INV-G10 (capture-only, לא משנה review_status) · G1/G2 ·
INV-IA (אותו שער/טאב). רגרסיה: 76 בדיקות עברו.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
מציג את התלבטות 3-השופטים (הצבעה+נימוק לכל לינאז' + ה-verdict)
בתוך כרטיס-האישור הקיים של דפנה ב-/precedents → "ממתין לאישור",
כדי שהכרעתה — תווית-הזהב שהלולאה לומדת ממנה — תהיה מיודעת ב*למה*
הפאנל נחלק. אושר ב-Claude Design (כרטיס 18-halacha-deliberation).
Backend (opt-in, ברירת-מחדל off — קוראים קיימים לא מושפעים):
- db.list_halachot(include_panel_round=True) → _annotate_panel_rounds
מצרף את הסבב האחרון מ-halacha_panel_rounds (DISTINCT ON, latest).
- GET /api/halachot?include_panel_round=true.
Frontend:
- Halacha.panel_round (טיפוס ידני; ה-endpoint מחזיר dict).
- תור-הסקירה (useHalachotPending) מבקש include_panel_round בשני
הדליים (clean=keep, needsFix=nli/entailed).
- רכיב PanelDeliberation: טבלת 3-שופטים (✓נתמך/✗הכלל-חורג + נימוק),
תג-ורדיקט "פיצול 2:1", ושורת "שורש המחלוקת" (קפדני↔תמצית) רק
בפיצול-entailment. מוזרק אחרי רשת הכלל/ציטוט.
שער יחיד — אין עמוד/שער חדש (INV-IA/G10); display-only, לא נוגע
ב-review_status. ולידציה: py_compile + tsc --noEmit + eslint נקיים;
בדיקה פונקציונלית: panel_round מצורף ל-6 שיש להן סבב, 1994 בלי.
חלק מ-#133 (FU-2). דורש deploy + (אופ') npm run api:types אחרי.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
המשך ל-#215. תור-הסקירה האמיתי (list_halachot) מסנן pending_review בלבד —
deferred ("נדחה למועד", #84 snooze) מוסט במכוון מהתור הפעיל. לכן ספירתו
כ"ממתין" צבעה שורות אדום על עבודה שדפנה כבר הסיטה הצידה — בדיוק ההטעיה
ש-#215 בא לתקן.
- backend: pending_count = pending_review בלבד (היה pending_review+deferred);
deferred_count חדש ונפרד. אותה שאילתה, מקור-אמת יחיד (G2).
- UI: deferred מוצג כמקטע מושתק (⏸ N) רק כשקיים — לא צובע אדום, לא נספר
בממתינות. הצבע האדום + רקע-השורה מונעים מ-pending_count (=pending_review)
בלבד, בעקביות עם התור.
Invariants: G2 (ספירה ממקור-אמת יחיד תואמת-תור). שינוי-UI לפי החלטת היו"ר.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
דף /archive הציג תיקים בסדר updated_at במקום לפי תאריך-הארכוב, למרות
שעמודת "תאריך ארכוב" סומנה כממוינת. השורש: list_cases() החזיר תמיד
ORDER BY updated_at DESC, וההסתמכות על מיון-בדפדפן (TanStack) לא הבטיחה
את הסדר בטעינה הראשונית.
התיקון: כש-archived_only=True → ORDER BY archived_at DESC NULLS LAST.
הסדר הופך server-authoritative; לא נוגע ברשימה הפעילה ולא ב-MCP tool
(שאינו מעביר archived_only).
Invariants: G1 (נרמול-במקור — סדר נקבע בשאילתה, לא תיקון-בקריאה),
G2 (לא מסלול-מקביל — אותו list_cases), INV-IA* (מקור-אמת יחיד לרשימה).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
כל הכרעת keep/drop חדה של היו"ר על הלכה שהפאנל כבר שפט (יש לה שורה
ב-halacha_panel_rounds) פולטת seed gold-set מתויג-יו"ר — הסיגנל היחיד
שמותר ללולאת הלמידה ללמוד ממנו. לימוד מהצבעות-הפאנל-עצמן = echo-chamber
ואסור; לכן הזרע נטבע אך-ורק מהכרעה אנושית.
- db.seed_goldset_from_chair(): capture-only, idempotent (UPSERT על
batch='chair-live', tagged_by='chair'), לעולם לא נוגע ב-halachot ולא
זורק שגיאה לתוך השער (INV-G10). ממפה approved/published→keep,
rejected→drop; deferred/pending_review = נודניק, בלי seed.
- db._chair_seed_label(): שער טהור (בלי DB) → guard echo-chamber
unit-testable; מסנן reviewer מכונה (panel:* / corroborated*).
- מחובר ב-db layer (update_halacha + update_halachot_batch) כך שכל
מסלולי-השער מתכנסים (G1 נרמול-במקור, G2 בלי מסלול מקביל). הפאנל
משתמש ב-SQL גולמי ולא ב-update_halacha → אין echo-chamber מבני.
- מצריך שורת-פאנל קודמת: ערך-הזרע הוא זוג (הצבעות-פאנל ⋈ הכרעת-יו"ר)
שמזין זיקוק-rubric (FU-4) ומדידה (FU-5).
- test_chair_seed_gate.py: 10 בדיקות offline על מדיניות-השער + guard.
Invariants: INV-G10 (שער-אישור יחיד, capture-only) · INV-LRN1
(propose-only — אין auto-commit) · G1/G2 · anti-echo-chamber (#133).
אין UI/שער חדש (INV-IA). תצוגת-הצבעות-הפאנל ב-HalachaReviewPanel
(אופציונלי) נדחית — מצריכה שער-עיצוב Claude Design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
התג בספריית-הפסיקה הציג "approved/total" — total כלל גם הלכות שנדחו,
כך ש-17/27 נקרא כאילו 10 ממתינות בעוד שבפועל הן נדחו. כעת:
- backend: list_external_case_law מחזיר pending_count (pending_review+deferred)
ו-rejected_count לצד approved_count (approved+published). מקור-אמת אחד
לספירה, אותה שאילתה — אין מסלול מקבילי (G2).
- UI: התג מציג 17/0/10 (מאושר/ממתין/נדחה), צבעי-משמעות, tooltip מפרש.
ממתין נצבע אדום-בולט רק כשגדול מ-0.
- UI: שורה עם הלכות ממתינות (pending_count>0) מקבלת רקע אדמדם דרך
rowClassName() — חל על שורות פסיקה ועל שורות ועדות-ערר.
Invariants: G2 (ספירה ממקור-אמת יחיד, ללא מסלול מקבילי). שינוי-UI ויזואלי
לפי אפיון מפורש של היו"ר (שער-עיצוב).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
לולאת ה-active-learning זקוקה לסיגנל ללמוד ממנו, אבל הפאנל
(halacha_panel_approve.py) זרק עד כה את הצבעות-3-השופטים ואת
ההנמקות — שרד רק review_status הסופי על halachot. בלי
ההצבעות+הנימוקים אין דרך לזקק rubric משופר.
FU-1:
- טבלה חדשה halacha_panel_rounds (SCHEMA_V35) — שורה לכל
(הלכה, סבב): הצבעה+נימוק לכל לינאז' (claude/deepseek/gemini),
ה-verdict, ומה הריצה עשתה (applied_action), apply_mode.
במתכונת עמודות-הפאנל של halacha_goldset.
- db.insert_panel_round() — helper כתיבה (capture-only).
- halacha_panel_approve.py: שומר את התשובות הגולמיות (במקום
לזרוק את הנימוק), מוסיף reason ל-NLI_SYSTEM, וכותב סבב לכל
פריט בשני המצבים (dry-run ו---apply). --no-capture לדילוג.
capture-only: לעולם לא נוגע ב-halachot — שער-היו"ר ב-/precedents
נשאר מקור-האמת היחיד (INV-G10). ה-seed ללמידה נוצר בהצלבה מול
הכרעת-היו"ר המאוחרת על אותה הלכה (FU-2).
Invariants: מקיים INV-G10 (capture-only, שער-יו"ר יחיד),
INV-LRN1/3 (לכידה-מבנית; propose-only — אין auto-commit),
G1 (לכידה-במקור), G2 (יכולת חדשה, לא מסלול-מקביל),
G12 (לא נוגע ב-Paperclip port). חלק מ-#133.
smoke (dry-run --limit 8): 6 nli captured, errors=0, נימוקים
מלאים מ-3 השופטים.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
מונה "ממתין (בקלוג)" ב-/operations הציג 140 פריטים תקועים שהדריינר (Gemini, כל
15 דק') דיווח עליהם total_pending=0 — אי-התאמה בין שתי הגדרות-תור:
ה-UI סופר status='pending' (ברירת-מחדל של העמודה), בעוד הדריינר סורק רק
metadata_extraction_requested_at IS NOT NULL. שורות שקיבלו מטא במסלול אחר
(internal דטרמיניסטי, cited_only חסר-טקסט) נשארו על ברירת-המחדל 'pending' לנצח.
פילוח ה-140: 82 internal_committee (מטא דטרמיניסטי, מחוץ לצנרת-Gemini) ·
31 cited_only (אין טקסט לחלץ) · 27 external_upload (כבר מלאים).
תיקון-במקור (G1 — נרמול במקור, לא תיקון-בקריאה):
- db.create_internal_committee_decision: INSERT + ON CONFLICT קובעים
metadata_extraction_status='completed' ישירות → שורות פנימיות לא נכנסות
שוב למצב-הרפאים.
- scripts/reconcile_metadata_status.py: נרמול חד-פעמי/re-runnable של שורות
קיימות (internal/external מלא→completed · external חסר→requeue · cited_only→skipped).
הורץ: 82+27→completed, 31→skipped, pending=0.
- web-ui /operations: התווית "ממתין (בקלוג)" → "ממתין" (הסרת המילה הלועזית)
+ tooltip מדויק; הערת operations.ts מעודכנת.
Invariants: מקיים G1 (normalize-at-source) ו-INV-IA (מונה-אמת/מקור-אמת-יחיד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
חילוץ החלטת-ה-dedup ל-helper טהור ובדיק `halacha_quality.dedup_action()` (skip/flag/keep),
ושני שיפורים על מסלול ה-dedup-on-insert:
#82.4 — merge-with-provenance, לא blind-drop: כשמדלגים על כפילות-סמנטית (cosine≥0.93),
מאחדים את ה-`cites` של השורה הנכנסת אל השכן הקנוני ששורד (במקום לאבד אותם). זהו שדה-ה-
provenance היחיד שקיים בהכנסה; בחירת-קנוני + מיזוג-corroboration מלא שייכים למסלול ה-
reconimation הלא-מקוון (#82.7 / #84.2, שם לשורות כבר יש provenance מצטבר) — מתועד בקוד.
#82.6 — over-merge guard: ההחלטה PAIRWISE מול שכן יחיד הקרוב ביותר, ורק השורה הנכנסת
מודלגת אי-פעם (אף שורה קיימת לא ממוזגת/נמחקת). אין connected-components closure בהכנסה,
לכן שרשרת A~B~C לא קורסת לשורה אחת גם כש-A,C מובחנים. מתועד ב-dedup_action + נבדק.
invariants: G1 (provenance נשמר במקור, לא אובד) · G2 (לוגיקת-החלטה ב-helper יחיד בדיק,
refactor משמר-התנהגות) · INV-G10 (אין auto-merge של שורות קיימות; tail→flag→סקירת-יו"ר).
tests: 6 חדשות (skip/flag/keep/over-merge/boundaries) + 59 בדיקות-הלכה קיימות עוברות.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
תיקון data-loss: reset_halacha_extraction ביצע DELETE ללא-תנאי לפני חילוץ-מחדש;
קריסה בין המחיקה לאחסון הראשון מחקה את כל אישורי-היו"ר והשאירה את הרשומה תקועה
status='processing' עם 0 שורות (תקרית עמיאל 8126-03-25, 2026-06-08).
עכשיו המחיקה מחריגה review_status IN ('approved','published') — אישור אנושי לא
נמחק בשקט (INV-G10). ה-dedup-on-insert של store_halachot_for_chunk מדלג על חילוץ
טרי שמשכפל מאושרת שנשמרה, כך שאין כפילות. reset מחזיר {deleted, preserved},
וה-extractor מתעד כמה מאושרות נשמרו (provenance, G9).
עמידות מלאה מול מוות-תהליך (OOM) נשארת ל-X16/#114 (durable resume) — זה תנאי-מקדים.
בדיקה: test_halacha_reextract_preserves_approved.py (offline SQL-capture) מאמת
שה-DELETE מחריג approved/published; 64 בדיקות-הלכה קיימות עוברות.
Invariants: G10 (שער-יו"ר — אישור לא נמחק), G1 (תיקון במקור), G9 (provenance).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
הדף הציג את התורים באופן לא-אחיד (by_status גולמי), בלי הבחנה בין "ממתין"
(בקלוג: status=pending) ל"בתור" (התור הפעיל: requested_at IS NOT NULL), בלי
הצגת הפריט שרץ כרגע, ובלי שום שליטה בתהליכים.
מה נוסף:
1. כרטיסי-תור אחידים — בתור / ממתין(בקלוג) / בעיבוד / הושלם / נכשל + "רץ עכשיו"
(citation/case_number של הפריט בעיבוד) לכל drain (אחזור-פסיקה, מטא-דאטה,
הלכות, יומונים). שערי-אנוש (אישור-הלכות, פסיקה-חסרה) נשארים מוני-סטטוס.
2. פאנל ניהול-תהליכים בסגנון "שירותי Windows":
- דמון (court-fetch-service/xvfb/chat/reaper): הפעל-מחדש / עצור / הפעל.
- cron drain: "הרץ עכשיו" (pm2 restart) + מתג הפעל/כבה תזמון.
3. כל תגי-הסטטוס מתורגמים לעברית.
מנגנון:
- הפעל/כבה תזמון = דגל ב-DB (טבלה drain_controls). pm2 cron_restart מחיה תהליך
שעוצר ב-stop, לכן ה"כיבוי" האמין הוא דגל שכל drain בודק ב-startup (no-op מיידי
כשכבוי). הקונטיינר כותב/קורא ישירות מ-DB.
- הרץ-עכשיו + restart/stop/start = proxy ל-pm2 דרך endpoint חדש בגשר-המארח
(court_fetch_service /pm2/control), מאובטח Bearer + whitelist ל-legal-* בלבד.
- יומונים: drain_digests הועבר מ-crontab ל-pm2 (legal-digest-drain.config.cjs)
כדי שיופיע ויהיה שליט כמו כל drain. drain_halacha_queue.py הובא לבקרת-גרסאות.
Invariants: מקיים G2 (הרחבת /operations + הגשר הקיים, לא מסלול מקביל) ו-G1
(drain_controls = מקור-אמת יחיד לכיבוי, נורמליזציה במקור ולא תיקון-בקריאה).
אין בליעת שגיאות שקטה (הגשר מחזיר {ok,error}; המוטציות מציגות toast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The halacha extraction queue was stuck (same class as the metadata issue): 26
precedents requested extraction with no drainer, plus 1 orphaned in 'processing'
(status=processing, requested_at cleared → never re-picked by the queue).
- db.requeue_stale_processing_extractions(kind): re-stamp orphaned 'processing'
rows (requested_at IS NULL) so they re-drain; halacha extractor force=False
resumes from chunk checkpoints (no duplicates).
- process_pending_extractions calls it at the top — fully unattended, safe under
the global advisory lock. Mirrors the digests-drain self-heal.
- legal-halacha-drain.config.cjs: pm2 cron (every 2h, conservative — Claude is
slow/rate-limited and each run adds to the chair's pending_review queue).
drain_halacha_queue.py stays on claude_session (high reasoning quality for
holding/ratio; NOT moved to Gemini). SCRIPTS.md.
The chair-approval gate (INV-G10) is untouched — this only produces halachot;
Daphna still approves each in /approvals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The extractor classified rule_type by SOURCE bindingness (higher-court→binding,
committee→persuasive) instead of by rule KIND. The gold-set proved it: 'binding'
appeared on 19/19 external rulings & 0 committees; 'persuasive' on 13/13
committees & 0 external — only 58% agreement with the human role tags. The two
axes (authority vs rule role) were crammed into one enum.
This splits them per INV-DM7:
- authority (binding/persuasive) — DERIVED from case_law.precedent_level
(עליון/מנהלי→binding, ועדת_ערר_מחוזית→persuasive), never stored, never
LLM-guessed. New helper halacha_quality.derive_authority; surfaced read-only
in list_halachot / goldset_list / search results.
- rule_type — now the rule ROLE only: holding/interpretive/procedural/
application/obiter. Both extractor prompts unified to this vocabulary;
_coerce_halacha no longer defaults rule_type from the source; legacy
binding→holding / persuasive→interpretive fold for safety.
UI: authority shown as a separate read-only badge (gold=מחייב / muted=משכנע)
across the review queue, precedent detail, and gold-set; the gold-set role
selector drops binding/persuasive and adds מהותי (holding).
Migration: scripts/halacha_rule_role_backfill.py re-classifies the 276 pre-split
binding/persuasive rows into a genuine role via local claude_session (run after
deploy). Gold-set correct_type/ai_correct_type 'binding'→'holding' via SQL.
Sources (≥3, per research-decision policy): OASIS LegalRuleML v1.0
(appliesAuthority/Strength as metadata orthogonal to rule logic) · SemEval-2023
Task 6 LegalEval (rhetorical roles by function, authority kept separate) ·
Bluebook signals (weight-of-authority is a separate dimension).
Invariants: ESTABLISHES INV-DM7. Upholds G1 (normalize at source — extractor
classifies role, system derives authority) and G2 (single source of truth —
authority derived, not a parallel stored field). Tests: 211 pass + new
derive_authority/coerce coverage. web-ui build + tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chair wanted an independent recommendation beside each tag, to reconsider
his own judgments. Adds a NON-ground-truth AI second-opinion:
- schema: halacha_goldset.ai_is_holding / ai_correct_type / ai_rationale /
ai_generated_at (additive).
- db.goldset_set_ai_recommendation + goldset_list now returns the ai_* fields.
- scripts/goldset_ai_recommend.py — local claude_session judges is_holding +
type + a one-line rationale per item, INDEPENDENTLY (own legal rubric).
Independent of the rule-based validators #81.8 measures → no circularity.
Never auto-applied; QA aid only.
- web-ui: each card shows "🤖 המלצת AI: הלכה/לא · type" + rationale and an
agreement/disagreement chip vs the human tag (amber on disagree); a
"⚠ אי-הסכמות AI (N)" filter to review only the conflicts.
Methodology note kept explicit: the human stays the ground truth; the AI is a
prompt to reconsider, not to copy.
Verified: tsc --noEmit 0; generator stores recs and flags disagreements with
existing human tags.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tagging is easier one source-type at a time. goldset_list now returns
case_law.source_type; the page adds:
- a filter (הכל / פסקי דין / ועדת ערר) with live counts,
- a group-sort so even in "הכל" all court rulings come first, then all
committee decisions,
- a per-card source badge (פסק-דין / ועדת ערר).
Verified: tsc --noEmit 0; source_type splits the live batch 58 court / 92 committee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the CSV-edit workflow with an in-app tagging page so the chair/Dafna
can label the extraction-quality gold-set by clicking, and see validator
precision/recall live.
Schema (V29): halacha_goldset — a stratified, human-tagged evaluation batch
(is_holding / correct_type / quote_complete, NULL until tagged).
db.py:
- goldset_create_sample (stratified round-robin over case×rule_type, idempotent),
- goldset_list (items + halacha content + the machine's own labels),
- goldset_tag (partial — one field at a time for keyboard tagging),
- goldset_score (ports the script's P/R/F1: each validator scored as a
not-a-holding detector against the human tags — the #81.8 input).
API: GET /api/goldset, POST /api/goldset/sample, GET /api/goldset/score,
PATCH /api/goldset/{id}.
web-ui:
- lib/api/goldset.ts (hooks),
- components/goldset/goldset-panel.tsx — card-per-item, keyboard-first
(J/K nav, H/N holding, C/X quote), progress bar, hide-tagged toggle, and a
collapsible live score table,
- app/goldset/page.tsx + nav link "מדגם-זהב" under ידע ולמידה.
Methodology guard kept explicit in UI + docstrings: tags are HUMAN ground truth,
no AI pre-fill (circular bias). Populated a 150-item stratified batch.
Verified: backend create/list/tag/score against the live DB; tsc --noEmit 0;
py_compile ok. (Local Turbopack build blocked by worktree symlink — CI builds clean.)
Invariants: G1 (eval set modeled at source in its own table); G2 (reuses the same
halacha_quality validators the extractor runs — no parallel scoring logic).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>