Commit Graph

290 Commits

Author SHA1 Message Date
c7c402e7ef feat(operations): manual burst control for the halacha drain + permanent supervisor
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
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).
2026-06-12 11:11:13 +00:00
959cb093b4 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>
2026-06-12 10:50:12 +00:00
2962538c09 feat: תיקון-ציטוט בדלי-החילוץ + קישור-לתור מדף-פרט (#133 follow-ups)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
אושר ב-Claude Design (כרטיס 20-halacha-followups).

א׳ תיקון-חילוץ אמיתי ל-quote_unverified:
- `update_halacha` מקבל `supporting_quote`; בעדכונו מריץ `_verify_quote`
  הקיים מול `case_law.full_text` השמור (דטרמיניסטי — בלי OCR/LLM מחדש,
  feedback_no_reocr_retrofit) ומסנכרן `quote_verified` + מוסיף/מסיר את
  הדגל `quote_unverified`. יו"ר שמדביק את הנוסח הנכון מהמקור → הדגל נמחק
  → ההלכה עוזבת את דלי-החילוץ. `HalachaUpdateRequest`+handler מעבירים את
  השדה; `HalachaPatch` + מצב-העריכה ב-HalachaCard כוללים textarea-ציטוט
  (נשלח רק כששונה) + hint.

ב׳ דף-פרט פסיקה — ביטול כפילות-המשטח:
- הלכה pending ב-`ExtractedHalachotSection` מציגה קישור "עבור לתור הלכות"
  במקום כפתורי אשר/דחה כפולים (שער-אישור יחיד, INV-IA/G10).
- `/precedents` Tabs הפך נשלט וקורא `?tab=review` (post-mount, בלי
  hydration-mismatch) כדי שהקישור ינחת על טאב-התור.

display-only ל-G10 (האימות מסנכרן מטא-איכות, לא review_status). ולידציה:
py_compile + tsc + eslint נקיים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:03:29 +00:00
b49cde7c24 feat(missing-precedents): חיפוש-טקסט לפי מראה-מקום בדף פסיקה-חסרה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
הבעיה: בדף /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>
2026-06-12 07:58:29 +00:00
4f7c3733e2 fix(ingest): read staged file via storage.ensure_local — s3-only upload 500 (INV-STG1)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
תיקון: העלאת פסיקה/החלטת-ועדה (precedent-library + internal-decisions) נכשלה
תחת backend s3-only עם "Package not found at '/data/...docx'" / "Converted file
not found". השורש: ‎`ingest._stage_file` כותב את הקובץ דרך ‎`storage.put_file`
ומחזיר נתיב‎-DATA_DIR, אבל תחת s3-only ה‎-blob נכתב רק ל‎-MinIO ואין עותק בדיסק —
ואז הצינור קרא את הנתיב ישירות מהדיסק (extract_text) → קובץ לא קיים. מסלול
תיקי‎-המקרה לא נפגע כי הוא שומר עותק‎-דיסק + mirror_file; רק מסלול ‎_stage_file
המשותף קרא את ה‎-key כאילו הוא על הדיסק.

התיקון (נרמול‎-במקור, G1; קריאה דרך שכבת‎-האחסון, INV-STG1):
- ‎`_stage_file` מחזיר עכשיו את ה‎-KEY (נתיב יחסי‎-DATA_DIR), לא Path.
- ‎`ingest_document` ו‎-‎`digest_library` מאתרים נתיב‎-קריאה מקומי דרך
  ‎`storage.ensure_local` (עותק‎-דיסק תחת filesystem/dual; הורדה ל‎-temp תחת
  s3-only) ומנקים את ה‎-temp ב‎-finally — בלי דליפה ל‎-/tmp.
- מולטימודל (PDF) קורא את אותו נתיב מקומי מאומת.

בדיקות: test_unified_ingest::test_ingest_reads_via_ensure_local_when_no_disk_copy
מדמה backend ללא עותק‎-דיסק ומוודא שהצינור משלים (נכשל מול הקוד הישן). 55 עוברות.

Invariants: מקיים INV-STG1 (קריאה/כתיבה רק דרך שכבת‎-האחסון), G1 (נרמול‎-במקור,
לא תיקון‎-בקריאה), G2 (אין מסלול מקביל — תיקון הצינור הקנוני).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 07:32:04 +00:00
ca1a0ddaac Merge pull request 'fix(learning): chair_name במקור — סופי-ועדה תמיד נכנס לקורפוס-הפסיקה (#134)' (#226) from worktree-chair-name-rootfix into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m30s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-12 07:26:32 +00:00
242e6cfd11 fix(learning): chair_name במקור — סופי-ועדה תמיד נכנס לקורפוס-הפסיקה (TaskMaster #134)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
הבאג: שלב-הלמידה (ingest_final_version → ingest_internal_decision) מוסיף כל
סופי כתקדים ציטוטי ב-case_law (source_kind=internal_committee), אך נכשל
בשקט (non-fatal warning) כש-cases.chair_name ריק — בגלל constraint
case_law_internal_chair_check. כך סופיים של 1194/1200/8070 לא נכנסו
לקורפוס-הפסיקה. שורש: (1) chair_name לא נקבע בפתיחת תיק; (2) מסלול-ה-MCP
העביר chair גולמי בעוד מסלול-ה-UI (web/) כבר פתר אותו דטרמיניסטית —
**מסלולים מקבילים מתפצלים (הפרת INV-G2)**; (3) הכשל נבלע (נגד §6).

תיקון-שורש (3 שכבות):
1. **SoT יחיד (INV-G2):** `config.committee_chair_for_case` — המקום היחיד
   שגם web/app.py וגם tools/workflow.py + db.create_case גוזרים ממנו chair
   (לפי תחילית מספר-התיק; override ל-env). web/ אחוד אליו (הוסרה הכפילות).
2. **נרמול-במקור (INV-G1):** `db.create_case` קובע chair_name תמיד לא-ריק;
   `cases.case_create` חושף param. `ingest_final_version` גוזר chair מה-SoT
   במקום הערך הגולמי → ה-constraint לא נופל.
3. **נראות (§6/feedback_silent_swallow):** כשל-העתק מוחזר ב-result
   (`internal_corpus_error`) ו-`final_learning_pipeline` מדפיס אזהרה — לא
   נבלע. backfill ל-11 תיקים עם chair ריק. `audit_corpus_integrity`:
   נוספו CHECK_D (תיקים מוכרעים ללא chair) + CHECK_E (סופי-final חסר
   מקורפוס-הפסיקה) — שניהם 0 כעת.

invariants: מקיים INV-G1 (נרמול בכתיבה), INV-G2 (מסלול-יחיד, אוחד web↔MCP),
§6 (אין בליעה שקטה). בדיקות: py_compile + 14 pytest (chair_seed_gate,
audit_provenance) + integration של create_case (default+override) + הרצת
ה-audit החי (A–E=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 07:25:54 +00:00
d246fb85fc feat(learning): FU-5 — מדידת לולאת-הלמידה מול הכרעות-היו"ר (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
מרחיב את halacha_panel_calibrate.py כדי למדוד את הלולאה לאורך-זמן ולשמור
על בריאותה — סוגר את 5 ה-FU של #133.

- --source captured (חדש, אפס-עלות): מצליב סבבי-פאנל שמורים (FU-1) מול
  הכרעות-היו"ר (FU-2) דרך db.panel_rounds_vs_chair, ומדווח split-rate +
  auto-precision + false-keep/false-drop **לכל סבב (per round-day)** מול
  ה-gold-set הגדל. כך רואים את הלולאה עובדת: ככל שהרובריקה משתפרת
  (FU-4 → אימוץ-יו"ר) — precision נשמר ו-split יורד. בלי re-vote, בלי LLM.
- summarize_calibration() + bucket_by_round() — עוזרים טהורים (offline-
  testable). משתפים את analyze_pairs של FU-4 → "מה נכשל" מחושב במקום אחד
  (בלי drift, G2).
- anon-stability: שתי המדידות מדווחות את שיעור-יציבות מבחן-האנונימיזציה
  (#81.7) כמטריקת-בריאות נגד echo-chamber — נפילה = שינון במקום היגיון.
- --source live (קיים): נוסף עמוד split-rate מפורש + anon-stability.
- tests/test_panel_calibrate_captured.py — 5 בדיקות offline. SCRIPTS.md
  עודכן. smoke read-only עבר (0 זוגות → nothing-to-measure).

Invariants: read-only מדידה · INV-G10 (האמת=הכרעת-יו"ר) · anti-echo-
chamber (anon-stability) · G2 (analyze_pairs מקור-יחיד). רגרסיה 30 עברו.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 07:19:48 +00:00
4cad17df3a feat(learning): FU-4 — זיקוק-רובריקה propose-only מהכרעות-היו"ר (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
job תקופתי שסוגר את לולאת-הלמידה: מצליב את סבבי-הפאנל (FU-1, הצבעות+
נימוקים) מול הכרעות-היו"ר (FU-2 seeds), מזהה כשלים שיטתיים, ומציע
KEEP_SYSTEM v2 + exemplars מופשטים — כדוח-diff לעיון-היו"ר. לעולם לא
auto-applied.

- db.panel_rounds_vs_chair() — read-only LATERAL join: לכל הלכה עם seed
  chair-live (FU-2, אמת אנושית) + סבב-פאנל אחרון (FU-1) → הצבעות+נימוקי-
  3-השופטים מול keep/drop של היו"ר. הסיגנל היחיד = הכרעת-יו"ר, לא
  הצבעות-הפאנל (anti-echo-chamber, INV-LRN1).
- scripts/halacha_rubric_distill.py:
  • analyze_pairs() — ליבה דטרמיניסטית טהורה (offline-testable): false-keep
    (פאנל שמר, יו"ר דחה), false-drop, פיצולים-שהוכרעו, שיעור-מחלוקת-עם-
    היו"ר לכל שופט; בוחר ראיות-מחלוקת מכוסות.
  • הצעת-LLM מקומית (claude_session, tools="", אפס עלות): מזהה דפוסי-כשל
    ומציע נוסח-רובריקה v2 + exemplars מופשטים (INV-LRN5 — בלי מהות-תיק).
  • כותב data/learning/rubric-proposal-<ts>.md עם diff(KEEP_SYSTEM→v2);
    אף שורת-קוד לא משתנה. אימוץ = עריכה ידנית דרך PR (INV-LRN1).
  • <12 זוגות → "אין מספיק נתונים" (מצב נוכחי: seeds עדיין מצטברים).
  • --no-llm (סטטיסטיקה בלבד) / --limit N.
- tests/test_rubric_distill.py — 8 בדיקות offline על analyze_pairs.
- SCRIPTS.md עודכן. smoke read-only עבר (0 זוגות → insufficient-data).

תואם הדפוס הקיים (style_lesson_panel/halacha_panel_audit): פאנל מציע,
הטמעה נשארת שער-יו"ר ידני. Invariants: INV-LRN1 (propose-only) ·
INV-LRN5 (טוהר-רובריקה) · INV-G10 · anti-echo-chamber. בלי שער/UI חדש.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 06:59:34 +00:00
d98ef14f41 feat(learning): FU-3 — uncertainty-sampling של תור-האישור לפי מחלוקת-הפאנל (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
תור-אישור-ההלכות הקיים (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>
2026-06-12 06:47:58 +00:00
d292c06ecd Merge pull request 'feat(learning): FU-2 UI — התלבטות-הפאנל במסך-אישור היו"ר (#133)' (#220) from worktree-halacha-deliberation-ui into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m46s
G12 Leak-Guard / leak-guard (push) Successful in 9s
2026-06-12 06:19:50 +00:00
9c6896df90 feat(learning): FU-2 UI — התלבטות-הפאנל במסך-אישור היו"ר (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
מציג את התלבטות 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>
2026-06-12 06:19:15 +00:00
e8bcb9c1ea fix(cases): מספור 5-ספרתי לבל"מ — סיווג, ולידציה, וחיפוש פסיקה-חסרה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
נוהל-יו"ר (2026-06-11): מבנה מספר-תיק = <סידורי>-<חודש>-<שנה>, ואורך הסידורי
מקודד את סוג-ההליך — 4 ספרות = ערר, 5 ספרות = בל"מ. הספרה הראשונה ממשיכה
לקבוע תחום בשני האורכים (1→רישוי, 8→היטל, 9→פיצויים). הכלל חד-כיווני:
5-ספרתי הוא תמיד בל"מ; 4-ספרתי אינו מחייב ערר (בל"מ-מורשת מזוהה מהנושא).

הבאג שדיווח עליו היו"ר: חיפוש פסיקה-חסרה לפי מספר-תיק החזיר 404 על כל ערך
שאינו תיק קיים — שבר את הטבלה תוך כדי הקלדה ועל מספרי 5-ספרות.

תיקונים:
- web/app.py: GET /api/missing-precedents — מסנן case_number שלא תאם תיק מחזיר
  רשימה ריקה (200), לא 404. סמנטיקה תקינה ל-collection-filter.
- missing-precedents/page.tsx: debounce (350ms) על שדות-הסינון — קוורי אחד
  אחרי שמפסיקים להקליד, לא אחד לכל הקשה.
- practice_area.py: regex סידורי \d{4}→\d{4,5}; case_serial_digits() +
  is_blam_by_number() (5⇒בל"מ); derive_subtype_with_blam ו-derive_proceeding_type
  מזהים בל"מ גם מ-5-ספרות (בנוסף לנושא). callers: cases.py, internal_decisions.py.
- proofreader.py: דפוסי חילוץ-שם-קובץ \d{3,4}→\d{3,5}.
- web-ui: practice-area.ts (מראָה ל-backend), schemas/case.ts (regex
  serial-month-year, 4-or-5 ספרות, superRefine 5⇒בל"מ), placeholder בוויזרד.
- תיעוד: docs/spec/X1-identifiers.md §1א + legal-ai/CLAUDE.md.

Invariants: מקיים G1 (נרמול-במקור — ספרה ראשונה כמקור-אמת יחיד לתחום),
G2 (מסלול-סיווג יחיד, אין כפילות), INV-DM/X1 (מפתח קנוני + proceeding_type).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 06:16:42 +00:00
9cd290e08e Merge pull request 'fix(precedents): deferred (snooze) לא נספר כ"ממתין" ולא צובע שורה אדום' (#218) from worktree-defer-not-pending into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m32s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-12 04:40:57 +00:00
b0411db80b fix(precedents): deferred (snooze) לא נספר כ"ממתין" ולא צובע שורה אדום
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
המשך ל-#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>
2026-06-12 04:40:26 +00:00
ebb9c211af Merge pull request 'fix(archive): מיון תיקי-ארכיב לפי תאריך-ארכוב (server-authoritative)' (#217) from worktree-archive-sort-fix into main
Some checks failed
G12 Leak-Guard / leak-guard (push) Has been cancelled
Build & Deploy / build-and-deploy (push) Has been cancelled
2026-06-12 04:40:20 +00:00
d4dc58fe5a fix(archive): מיון תיקי-ארכיב לפי תאריך-ארכוב (server-authoritative)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 7s
דף /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>
2026-06-12 04:39:16 +00:00
614c06ab60 feat(learning): FU-2 — לכידת seed אקטיב-לרנינג בשער-היו"ר הקיים (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
כל הכרעת 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>
2026-06-12 04:37:15 +00:00
4ea6326766 fix(precedents): פיצול תג-הלכות ל-מאושר/ממתין/נדחה + הדגשת שורות-טיפול
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
התג בספריית-הפסיקה הציג "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>
2026-06-12 04:29:07 +00:00
0a7869175e feat(learning): FU-1 — לכידת סבבי-פאנל להלכות (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 7s
לולאת ה-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>
2026-06-12 04:22:48 +00:00
b447ffb184 fix(ops): ייבוש backlog-רפאים של חילוץ-מטא — נרמול-במקור של metadata_extraction_status (G1)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
מונה "ממתין (בקלוג)" ב-/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>
2026-06-11 22:09:38 +00:00
6e69c1dc38 feat(ia): IA גל-2 — איחוד-משטחים: ערוץ-למידה אחד · /operations⊇/diagnostics · MET-2/3 (#131, X17)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
גל-2 מבקלוג #127 — איחוד-משטחים לפי משטח-היעד של X17. מקיים INV-IA1/IA3/IA4 +
דלתות-הספ (X6 INV-UI7/8, 07-learning §0.4, 00-constitution G2). שומר G10/INV-LRN1
(לא הוסר שום שער-אנושי — רק שער/דגל כפול).

א) תיבת-אישור אחת (INV-IA1): כרטיסי "אישור הלכות"+"פסיקה חסרה" ב-/operations
   מצביעים ל-/approvals (לתיבת-האישורים ←) — /operations מנטר, /approvals מחליט.

ב) ערוץ-למידה אחד (INV-IA3): הוסר applied_to_skill end-to-end —
   - UI: כפתור "סמן כ'אומץ'" + badge "אומץ" ב-lessons-tab; badge ב-curator-portrait.
   - API: LessonPatch, _lesson_to_json, patch call, curator recent_findings (→review_status).
   - db.py: list/add/update_decision_lesson לא בוחרים/כותבים applied_to_skill;
     הפרמטר הוסר. העמודה+אינדקס נשמרים (back-compat, ללא migration), מסומנים DEPRECATED.
   - types: DecisionLesson/LessonPatch/CuratorFinding.
   review_status='approved' = הסטטוס היחיד "זורם-לכותב" (INV-LRN1, #126).

ג) MET-2/3 lost-update (INV-IA3): _append_methodology_override רץ עכשיו בטרנזקציה
   אחת עם SELECT ... FOR UPDATE — אין read-modify-write מתפצל מול עורך-המתודולוגיה
   או promote מקביל. /methodology = העורך-הקנוני; promote מבטל את ה-cache (גל-1 MET-1).

ד) /operations⊇/diagnostics (INV-IA4): גוף /diagnostics חולץ ל-<SystemHealthSection/>
   ומורנדר ב-/operations תחת "בריאות-מערכת". /diagnostics → redirect ל-/operations.
   /diagnostics הוסר מהניווט. משטח-ניטור יחיד.

ה) דלתות-ספ (≥3 מקורות ב-X17, אושר ע"י חיים /goal):
   - X6: INV-UI7 (aggregate=SSoT, mutation מבטל queryKey) + INV-UI8 (render-or-remove, חלקיות).
   - 07-learning §0.4: שער-אחד + טרנזקציה-אחת + applied_to_skill מוסר.
   - 00-constitution G2: תאום-המתודולוגיה כהפרה-ידועה-ממותנת.
   - X17 דלתות-ספ סומנו  קודדו.

בדיקות: py_compile app.py + db.py ✓ · tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109
קיים-מראש). next build נכשל ב-worktree רק בגלל symlink (Turbopack) — Docker/CI תקין.
api:types יתרענן בדפלוי (curator/lessons אינם response-modeled; הטיפוסים יד-כתובים עודכנו).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:04:57 +00:00
0d8cc31a2b feat(storage): seal INV-STG1 write path — 15 dual-write seals + CI leak-guard + tripwire
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
אחרי ה-cutover ל-s3-only, אודיט מצא 15 אתרי-כתיבת-בלוב שעוקפים את storage.py (uploads/
finalize/exports/training/research-backup/precedents/bulletins/draft) — קובץ ינחת
בתיקיות-הישנות אך **לא** ב-MinIO → יאבד בניקוי, לא מוגש, לא מגובה. ה-pipeline (ingest/
extract) עדיין קורא לפי file_path מהדיסק, אז ביטול-מוחלט של כתיבה-לדיסק דורש read-wiring
מלא (Phase 2, משימה נפרדת). תיקון בטוח עכשיו = **dual-write seal**.

- storage.py: `mirror`/`mirror_file` (+ sync) — best-effort persist ל-S3 כשה-backend
  s3/dual (no-op ב-filesystem; כשל S3 נרשם, לא שובר request — DualBackend philosophy).
- web/app.py: helpers `_seal_blob`/`_seal_blob_file` + 14 אתרים אטומים (storage.mirror
  אחרי כתיבת-הדיסק; הדיסק נשאר ל-pipeline). block_writer.py: draft אטום (async).
- **CI leak-guard** (test_storage_write_leak_guard): נכשל על כל כתיבת-בלוב-לדיסק
  (write_bytes/write_text/shutil.copy*/open(wb)) ב-web/+services ללא מרקר `# noqa: STG1`.
  כל ה-benign (fallbacks/tmp/staging/git-metadata/flag/state) מסומנים עם נימוק. storage.py
  מוחרג (הוא המימוש).
- **tripwire** (scripts/storage_leak_tripwire.py): ניטור-ריצה — בלובים בדיסק שלא ב-MinIO
  (json-key match, bucket per-file). אומת חי: 0 דליפות.

invariants: INV-STG1 (כל I/O דרך storage / ממורר אליו) · INV-STG6 · feedback_silent_swallow
(mirror רושם warning, לא bare-except). Phase 2 (read-wire ה-pipeline → להפיל את עותק-הדיסק)
= follow-up. tests: 4 mirror + 1 leak-guard + 6 serve_blob + 18 storage קיימות עוברות.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:57:12 +00:00
4b01283e3b feat(learning): שער-אישור ל-decision_lessons — רק לקח מאושר זורם לכותב (INV-LRN1, #126)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 12s
אודיט #122 חשף שלקחי-הפאנל (decision_lessons) זרמו לכותב אוטומטית
(block_writer → get_recent_decision_lessons) ללא סינון-אישור — הפאנל כתב,
והכותב צרך מיד, בעקיפת שער-היו"ר (INV-LRN1/G10). מנגד, מה שהיו"ר אישר ב-promote
הלך לערוץ נפרד (appeal_type_rules). תוצאה: דליפה — תוכן לא-מאושר השפיע על הכתיבה.

התיקון — שער-אישור מפורש:
- עמודת review_status (proposed|approved|rejected) ל-decision_lessons (SCHEMA_V34).
- get_recent_decision_lessons (צרכן-הכותב) מחזיר רק review_status='approved'.
- הפאנל (style_lesson_panel) כותב 'proposed' (ברירת-מחדל) → לא זורם עד אישור.
- לקח שהיו"ר מקליד ידנית ב-/training = 'approved' מיידית (מדלג על שער-ההצעה).
- UI (lessons-tab, טאב "קורפוס" ב-/training): תג-סטטוס + כפתורי אשר/דחה/בטל-אישור.

הכרעת-יו"ר (2026-06-11): כל הלקחים שקדמו לשער (41) מתאפסים ל-'proposed' —
שום לקח לא זורם עד אישור מפורש (ברירת-המחדל של העמודה מיישמת זאת על הקיימים).

Invariants:
- INV-LRN1 / G10 (מקיים) — עדכון-ידע לערוץ-הכותב דורש אישור-יו"ר מפורש; אין auto-commit.
- INV-LRN5 (נשמר) — substance ממילא מסונן בפאנל; השער הוא על style_method בלבד.
- G1 (מקיים) — סינון-במקור (get_recent) ולא תיקון-בקריאה אצל הכותב.
- G2 (מקיים) — אותו פנקס decision_lessons; אין מסלול מקביל.

api:types: להריץ npm run api:types אחרי deploy (review_status נוסף ל-payload;
הטיפוסים הידניים ב-training.ts כבר מעודכנים, tsc עובר).

ref: #122 · #126 · data/audit/learning-loop-activity-20260611.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:13:59 +00:00
94a4c3600e fix(learning): process_final_version מאחסן דיסטילציה גם כשאין pair (create-or-update, INV-LRN4)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
אודיט #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) <noreply@anthropic.com>
2026-06-11 17:07:41 +00:00
ec14e8310b feat(halacha): #86.2 nevo-leak audit + safe ratio backfill · #86.3 ratio-coverage benchmark
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
#86.2 — scripts/nevo_corpus_audit.py leak: סורק chunks+הלכות למרקרי-preamble של נבו
(מיובאים מ-extractor._NEVO_MARKERS — מקור-אמת יחיד), מבחין בין הווקטור המזיק (מרקר בתוך
הלכה = רציו-עריכה שזוהה כהלכה) ל-benign (chunk עם רשימת-ציטוטים). **ממצא חי: 0/~1650
הלכות מזוהמות** — שכבת-הידע נקייה (שערי-האיכות של #81 מנעו זאת). לכן **אין purge/re-ingest**
(גם כי re-OCR retrofit נוגד-עיקרון, feedback_no_reocr_retrofit; וצ'אנקי-ציטוטים benign).
`leak --apply` עושה backfill **אדיטיבי** של case_law.nevo_ratio מ-full_text השמור
(extract_nevo_ratio, דטרמיניסטי, ללא re-OCR, לא נוגע בצ'אנקים/הלכות) — "לשמור במקום
למחוק". הורץ: 16→32 פסקים עם רציו שמור.

#86.3 — benchmark: לפסקים עם nevo_ratio, הפאנל התלת-מודלי שופט אילו עקרונות-רציו מכוסים
ע"י ההלכות שלנו → recall. smoke: 1110-20 (13 הלכות) recall=1.0 (כיסוי מלא); פסקים עם
0 הלכות → recall=0 (אות-פער-חילוץ אמיתי, לא כשל-כיסוי). מזין את אות-האיכות של #81.7.

invariants: G2 (מרקרים+strip מיובאים מ-extractor; פאנל מ-halacha_panel_approve) ·
INV-G10 (read-only/אדיטיבי; אין מחיקה) · no-reocr (backfill מטקסט שמור, לא חילוץ-מחדש).
tests: 6 offline (_has_marker/_has_editorial) + nevo_preamble קיים. אומת חי.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:50:50 +00:00
a4b4ebbbb1 feat(halacha): #84.7 — queue throughput + quality metrics
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
הרחבת metrics.halacha_backlog (G2 — אותה פונקציה, אין מסלול-מטריקות מקביל; כבר מוגשת
דרך /api/system/diagnostics) במדדי-תור שחסרו:
- throughput_24h / throughput_7d — קצב-ההחלטות (reviewed_at בחלון).
- approve/reject/defer ratios (קודם רק approve).
- median_seconds_per_decision — זמן-חציוני-לפריט, מחושב רק על פערים [1ש',30דק'] כדי
  לבטא קצב-אנושי אינטראקטיבי (פער-0 של batch panel/auto מוחרג, וגם פערים >30דק' בין
  sessions). 41.4s בייצור; None כשהתור כולו batch.
- by_reviewer — פילוח panel/auto/chair/other (מי החליט).
spot-check post-hoc כבר מכוסה ע"י halacha_panel_audit.py (re-judge של מאושרי-פאנל).
_median חולץ כ-helper טהור ובדיק.

invariants: G2 (הרחבת מטריקה קיימת) · INV-QA1/G10 (נראות שער-האנוש — גם מהירות וגם איכות).
tests: 4 offline (_median) + אומת חי על ה-DB (476 pending, throughput 115/956, median 41.4s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:42:22 +00:00
a00e226a08 Merge pull request 'feat(halacha): #82.4 provenance-union על dedup-skip + #82.6 over-merge guard' (#192) from worktree-halacha-dedup-provenance-guard into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 5s
2026-06-11 16:35:05 +00:00
97271689ef feat(halacha): #82.4 provenance-union on dedup-skip + #82.6 over-merge guard
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
חילוץ החלטת-ה-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>
2026-06-11 16:34:47 +00:00
4e06662208 feat(halacha): #81.8 — calibrate auto-approve gate on the gold-set (keep 0.80, documented)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
כיול סף-האישור-האוטומטי מול ה-100 תוויות-היו"ר (93 keep / 7 drop), אמת אנושית (לא
הקונצנזוס — מונע מעגליות):
  conf≥0.80 → P=0.98 R=0.53  ← נוכחי (errs safe)
  conf≥0.75 → P=0.96 R=0.81
  conf≥0.70 → P=0.94 R=0.94
  panel unanimous-3/3 → P=0.988 cov=95% · majority-2/3 → P=0.948 cov=100%

הכרעה: **לשמר 0.80** — עומד ביעד precision≥0.90 עם מרווח, וטועה לכיוון היו"ר
(recall נמוך = יותר סקירה, לא פחות). שני ממצאים:
 (א) self-confidence מכויל היטב ל-precision; הוולידטורים ה-rule-based לא-מבחינים
     על ה-gold-set (P≈0.1) → "confidence × validators" רק יזיק, לא אומץ (תשובה ל-#81.8).
 (ב) מנוף-הכיסוי האמיתי = הפאנל התלת-מודלי (unanimous 0.988/95%), לא סף-confidence נמוך.
     הורדת השער ל-0.75 = tradeoff governance (יותר auto-approve לא-מסוקר, INV-G10) על
     ראיה דקה (7 שליליים) → נדחה ליו"ר/פאנל (#121), לא שונה כאן.

- db.goldset_calibrate(): sweep-confidence + panel-policy precision/coverage מול הזהב,
  read-only, משוחזר (INV-LRN3). ground_truth='chair' default (אנטי-מעגליות).
- config: הערת HALACHA_AUTO_APPROVE_THRESHOLD מעודכנת לממצא-הכיול (במקום spot-check-of-10).

invariants: INV-G10 (לא הורדנו את השער הלא-מסוקר) · INV-LRN2/LRN3 (כיול מתועד במקור, מובנה).
tests: 4 offline (sweep/policies/anti-circularity/threshold-surfaced). אומת חי: משחזר את המספרים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:29:24 +00:00
5f93c7492f fix(halacha): #81.7 — report Gwet AC1 + consensus-vs-human (κ paradox under skew)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
ריצת-הפאנל החיה חשפה Fleiss κ=-0.07 למרות 97.5% הסכמה-גסה (28/40 פה-אחד, 11/40 רוב).
זה אינו חוסר-אמינות אלא **פרדוקס-הקאפא**: ה-marginal של is_holding מוטה קיצונית
(≈הכול True, כמו 93/100 ה-keep בתוויות-האנוש), וכש-Pe→1 גם κ→0 (Feinstein & Cicchetti
1990, "high agreement, low kappa").

- gwet_ac1(): מדד הסכמה עמיד-שכיחות (Gwet 2008) — אותו Pa כמו Fleiss, אומדן-מקריות שונה
  (2·p·(1-p)). הופך לכותרת; Fleiss κ עדיין מודווח לשקיפות + raw 3/3.
- consensus-vs-HUMAN: כשקיים תיוג-יו"ר, הדוח מודד התאמת-הקונצנזוס מולו (תוקף חיצוני).
  אימות בפועל על 100 תוויות-היו"ר: 29/29 = 100% התאמה.

invariants: ללא שינוי בהתנהגות-הכתיבה; מטריקה בלבד. tests: 21 (3 חדשות, כולל מקרה-פרדוקס מפורש).
מקור: Gwet 2008 (AC1) · Feinstein & Cicchetti 1990.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:13:24 +00:00
e6c6237ef6 Merge pull request 'feat(halacha): #81.7 — תיוג gold-set בקונצנזוס תלת-מודלי (Opus+DeepSeek+Gemini), κ + אנונימיזציה' (#188) from worktree-goldset-tri-model-consensus into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-11 16:04:04 +00:00
5b001bbd9d feat(halacha): #81.7 — gold-set labeled by tri-model consensus (Opus+DeepSeek+Gemini)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
מבטל את ה-man-in-the-loop בתיוג ה-gold-set (הנחיית-יו"ר 2026-06-11): במקום תיוג ידני
של חיים/דפנה, אמת-המידה נקבעת בקונצנזוס שלוש שושלות-מודל עצמאיות — אותו פאנל שמערכת
האישור החיה כבר משתמשת בו (halacha_panel_approve), עם 92% הסכמה חוצת-מודלים על הציר הגס.

למה לא מעגלי: הוולידטורים הנמדדים ב-#81.8 (compute_quality_flags / is_fact_dependent /
is_quote_truncated / is_thin_restatement) הם היוריסטיקות **rule-based** — משפחת-שיטה שונה
מה-LLM-judges. שני שומרי-יושר: (1) פיצול-קולות (אין רוב 2/3) לא כותב לייבל — הפריט נשאר
NULL ומוסלם ליו"ר (INV-G10); (2) מבחן-אנונימיזציה — שיפוט-מחדש עם מזהה-התיק ממוסך, flip
בקונצנזוס = שינון ולא הנמקה (arXiv:2505.02172).

- db.py: עמודות per-lineage (ds_*/gm_*; ai_*=claude קיים) + consensus/agreement/anon +
  goldset_set_panel_label() שכותב רוב-2/3 ל-is_holding/correct_type (tagged_by='panel:…',
  לא דורס tagged_by='chair'). goldset_score נשאר ללא שינוי — קורא is_holding (G2, אין מסלול
  ניקוד מקביל). עדכון הערת-הסכמה (בוטלה דרישת "MUST be human").
- scripts/goldset_panel_label.py: 3 שופטים (מיובאים מ-halacha_panel_approve, מקור-אמת יחיד)
  + prompt עשיר (מיובא מ-goldset_ai_recommend) + Fleiss κ + מבחן-אנונימיזציה. דוח→data/audit/.
- SCRIPTS.md: סקריפט חדש; goldset_ai_recommend/independent_judge מסומנים single-model נבלעים.

invariants: G2 (שופטים+prompt מיובאים, אין כפילות; ניקוד יחיד) · INV-G10 (פיצול→יו"ר) ·
INV-LRN2/LRN3 (איכות-במקור, לכידה מובנית). מקור: PoLL · Trust-or-Escalate (ICLR 2025) · arXiv:2505.02172.
tests: 18 offline (consensus/type/Fleiss-κ/anonymize). live labeling = צעד תפעולי אחרי deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:03:32 +00:00
3c169a76f2 feat(halacha): rhetorical-role pre-filter — fallback excludes facts/arguments (#81.6)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
חילוץ-הלכות מוגבל למקטעי הנמקה/הכרעה בלבד (INV-LRN2 quality-at-source). הפער שנסגר:
מסלול ה-fallback (כשה-chunker לא תייג שום מקטע כ-extractable, כותרות לא-תקניות →
הכול 'other') נפל קודם ל**כל** ה-chunks — והחזיר בדיוק את המקטעים שהמסנן הראשי מחריג
(רקע עובדתי + טענות הצדדים). בלבול Facts↔Reasoning הוא מחלקת-השגיאה הדומיננטית
(LegalSeg), כך שהזנת עובדות לחילוץ פוגעת ישירות ב-precision.

- NON_REASONING_SECTIONS = (facts, appellant_claims, respondent_claims, intro)
- _select_extractable_chunks(): מרכז את מדיניות-הבחירה (primary + fallback) בפונקציה
  אחת המשמשת גם את הבחירה הראשית וגם את ה-re-read לקביעת-סטטוס (G2 — מקור-אמת יחיד,
  אין מסלול מקביל). ה-fallback מחריג את NON_REASONING_SECTIONS ועדיין מגיע להנמקה
  שנחתה תחת 'other'.

invariants: G1 (נרמול-במקור, לא תיקון-בקריאה) · G2 (אין מסלול מקביל) · INV-LRN2 (quality-at-source).
tests: 4 חדשות (primary/fallback-excludes-args/all-nonreasoning/disjoint-sets) + 61 בדיקות-הלכה קיימות עוברות.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:52:13 +00:00
64db643e6d fix(writer): disable tools on block_writer + style_analyzer claude_session calls
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
המשך ל-#182 — שני האתרים שנותרו עם query()‎ ליצירת-טקסט/ניתוח, ששמרו על
ברירת-המחדל של ה-CLI (כל הכלים פעילים) ולכן חשופים לאותו error_max_turns:
המודל פולט stop_reason:"tool_use", מפיל את --max-turns 1, ומאלץ retry יקר.

- block_writer.py:413 — כתיבת פרוזת בלוק (Opus/Sonnet). יצירת-טקסט טהורה,
  אף פעם לא צריך כלי.
- style_analyzer.py:166/183/196 — single/multi-pass + synthesis; הפלט מפוענח
  כ-JSON (_parse_and_store_patterns/_extract_json). text→JSON טהור.

מיישר את שני האחרונים לאותו מסלול קנוני (claude_session.query(tools="")).
עכשיו כל קריאות ה-LLM שאינן צריכות כלים מעבירות tools="".

Invariants: מקיים INV-G2 (מסלול קנוני יחיד; סימטריה). אין בליעה שקטה (§6).
ללא שינוי-ספ.

בדיקות: py_compile נקי; 18 בדיקות (block/style/writer) עוברות.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:03:37 +00:00
d05c1e3fce fix(extractors): disable tools on text→JSON claude_session calls (no error_max_turns)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
כל קריאות text→JSON ב-9 המחלצים העבירו את ברירת-המחדל של ה-CLI (כל הכלים
פעילים). המודל פלט מדי פעם stop_reason:"tool_use", מה שמפיל את --max-turns 1
ל-error_max_turns ומאלץ retry — ~$0.12-0.16 לניסיון, × 3. נצפה ב-drain
חילוץ-ההלכות (legal-halacha-drain, ‎15 כשלי error_max_turns ב-error.log).

התשתית כבר קיימת: claude_session.query מקבל tools=""‎ לנטרול כל הכלים, ושני
מחלצים (digest_metadata_extractor, bulletin_splitter) כבר משתמשים בו. כאן רק
מיישרים את שאר המחלצים לאותו מסלול קנוני — אף קריאת חילוץ/שיפוט/סיווג טהורה
לא צריכה כלי.

מתוקנים (11 קריאות, 9 קבצים): halacha_extractor (×3: extract/NLI/consolidate),
corroboration, claims_extractor, argument_aggregator, appraiser_facts_extractor,
learning_loop, qa_validator, brainstorm, style_metadata_extractor.

Invariants: מקיים INV-G2 (מסלול קנוני יחיד; סימטריה בין מחלצים-אחים) — לא מסלול
מקביל חדש אלא שימוש עקבי בפרמטר הקיים. אין בליעה שקטה (§6) — נתיבי הכשל/retry
נשמרים. ללא שינוי-ספ.

בדיקות: 60/60 ב-tests/test_halacha_coerce.py + test_halacha_quality.py עוברות;
py_compile נקי על כל 9 הקבצים.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:49:35 +00:00
f5650196b7 Merge pull request 'feat(pipeline): עמידות (LangGraph) ל-final_halacha (P0, X16/INV-DUR1, #114)' (#178) from worktree-langgraph-durable-pipeline into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 7s
2026-06-10 09:53:07 +00:00
e7d8b24d7c feat(pipeline): durable execution for final_halacha via LangGraph (P0, X16/INV-DUR1, #114)
scripts/_pipeline_runtime.py — runtime עמידות משותף: עוטף רשימת-צעדים async ב-LangGraph
StateGraph ליניארי עם AsyncSqliteSaver (checkpoint לכל צעד). קריסה/OOM ממשיכה מהצעד
שנכשל במקום להריץ הכל מחדש. degradation חיננית: ללא langgraph → ריצה ליניארית כמו קודם
(הכפתור לא נשבר). מימוש אחד לשני הפייפליינים (G2).

final_halacha_pipeline.py — 4 הצעדים ([0]extract [1]citations [2]corroboration [3]panel)
רצים דרך ה-runtime. CLI זהה + --fresh (ברירת-מחדל auto-resume). thread יציב לכל תיק;
dry-run = preview נפרד (תמיד fresh). קריסה בפאנל [3] → resume מ-[3] (steps 0-2 שמורים).

pyproject: extra "durable" (langgraph + langgraph-checkpoint-sqlite) — host-only,
optional. data/checkpoints/ ב-.gitignore.

גבול (X16 §1): LangGraph רק כמנוע-פנימי של הסקריפט — לא orchestrator (לא מסלול מקביל
ל-Paperclip; G2/G12). #108 (atomic extract) קדם לזה כתנאי.

אימות: test_pipeline_runtime.py — עם langgraph (venv-זמני): 3 passed (resume מדלג צעדים
שהושלמו · fresh מריץ-מחדש · linear). בלי langgraph (venv משותף): 1 passed + 2 skipped
(degradation). final_halacha מתקמפל ומיובא נקי בשני המצבים. הרצה end-to-end על הפייפליין
החי (DB+LLM) — לאחר `pip install -e ".[durable]"` בעץ הראשי.

Invariants: INV-DUR1 (עמידות), G2 (runtime יחיד), G3 (idempotency מחוזק).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:52:35 +00:00
d2b622f28e feat(ci): G12 leak-guard — enforce the Agent Platform Port seam (R4, #113)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
המאכף האוטומטי של INV-G12 (docs/spec/X15 §4). שני כללים קשיחים:
1. mcp-server/src (שכבת-האינטליגנציה) ללא סמלי-Paperclip — allowlist מנומק לפי
   substring ל-6 ההפניות הלגיטימיות (pm2-bridge + הערות-מקור company_id).
2. import seam — רק web/agent_platform_port.py (+ קבצי-המעטפת) מייבאים paperclip_*.

מימוש קנוני אחד (scripts/leak_guard.py, stdlib-בלבד), משותף לשלושה אכיפנים (G2):
• CI hard gate: .gitea/workflows/leak-guard.yaml (pull_request + push→main)
• pytest: mcp-server/tests/test_platform_port_leak_guard.py (כולל self-test שמוודא
  שה-guard תופס הזרקה — לא ירקב)
• hook בזמן-אמת: spec-guard.sh בודק את התוכן-הנכתב (new_string/content) על כתיבה
  ל-mcp-server/src ומזהיר על הזרקת-Paperclip (לא-deduped); תזכורת-הספ עודכנה ל-G1–G12.

מחריג קבצים-נוצרים (web-ui types.ts) ומעטפת מוצהרת; הפרונט מחוץ להיקף-האינטליגנציה
(ממצא R3). עודכן scripts/SCRIPTS.md.

אימות: סריקה נקייה exit 0; הזרקת pc.sh ל-mcp-server → exit 1; seam-violation ב-web → exit 1;
hook מזהיר על mcp-server ומזכיר-ספ על web; pytest 3 passed; bash -n + YAML תקינים.

Invariants: G12 (אכיפה), G2 (מאכף יחיד לשלושה צרכנים).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:40:42 +00:00
26e0219219 fix(halacha): re-extraction preserves chair-approved halachot (INV-G10, #108)
תיקון 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>
2026-06-10 09:08:16 +00:00
d4d2ab4d68 feat(arguments): פופאפ פרופוזיציות גולמיות בלחיצה על "מסתמך על N"
הקישור טיעון↔פרופוזיציות כבר נשמר ב-DB (legal_argument_propositions),
אך ה-UI הציג רק את המספר. מעשיר את get_legal_arguments באותו round-trip
(JOIN ל-claims) להחזיר supporting_propositions = {id, text, source_document},
ועוטף את שורת "מסתמך על N פרופוזיציות" ב-Popover שמציג את הטענות הגולמיות
verbatim עם מקור. שקיפות ועקיבוּת מהטיעון המאוגד חזרה לטענות-המקור.

- supporting_claims נשאר id-only (תאימות לאחור: מונה, צרכני MCP)
- supporting_propositions שדה חדש אופציונלי; fallback לטקסט סטטי כשחסר
- אין מסלול מקביל (G2) — העשרה של אותו endpoint; נרמול-במקור (G1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:51:09 +00:00
d23f854c25 fix(ops): self-restart/stop of the host bridge returns 200 (detached)
Restarting/stopping legal-court-fetch-service from its own /pm2/control kills
the process before it can reply — the client got a misleading 502 even though
pm2 performed the restart. Detach the self-action (sleep 1; pm2 ...) so the HTTP
response flushes first, and report success optimistically. Other targets are
unchanged. Own name via COURT_FETCH_SERVICE_PM2_NAME (default legal-court-fetch-service).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:09:08 +00:00
638eef6803 feat(ops): /operations — מוני-תור אחידים, "מה רץ עכשיו", וניהול-תהליכים
הדף הציג את התורים באופן לא-אחיד (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>
2026-06-08 08:57:23 +00:00
b2ea0c28dd feat(storage): X14 Phase 2c — route remaining sync write-sites through storage.py
Completes the write-side rewiring (INV-STG1) for the call-sites that run in
synchronous contexts, via a new blocking facade in storage.py
(put_bytes_sync / put_file_sync — asyncio.run, or a worker thread when a loop
is already running):
- services/extractor.py: multimodal thumbnail JPEGs → DERIVED (rendered in a
  to_thread worker)
- services/docx_reviser.py: track-changes save (_save_docx_xml) + empty-diff
  copy (copy_with_revisions) → DOCUMENTS
- services/docx_retrofit.py: in-place retrofit backup → DOCUMENTS

Each site keeps a fallback to a direct disk write when the target path is
outside DATA_DIR (caller-provided). Under the default STORAGE_BACKEND=
filesystem the bytes land exactly where they did before — zero behaviour
change.

Also: mcp_env_catalog MINIO_ENDPOINT default updated to the durable
container-name endpoint (http://minio-bx2ykvw94xbutsex41hz4vv8:9000), matching
the Coolify "Connect to Predefined Network" change made for network durability.

All binary write-sites now flow through storage.py. git-tracked text
(case.json/notes/research-md/draft-md) stays on disk by design (INV-STG7);
court-fetch temp files are ephemeral.

tests: +2 (thumbnail renderer routes through storage; put_bytes_sync
round-trip); 55 storage/docx/track-changes green; 244 collected, no import
breakage.

Keeps G2; completes INV-STG1 write coverage. Spec: docs/spec/X14-storage-minio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:26:09 +00:00
5745d36bb4 feat(digests-ui): publication filter + 'מאמר'/source badges for bulletins
משלים את #154 בצד-לקוח:
- פילטר "מקור" בדף /digests (כל המקורות / כל יום / עו"ד על נדל"ן) — backend:
  list_digests + /api/digests מקבלים publication.
- DigestCard: תג "מאמר" ל-digest_kind='article', ו-chip מקור לפרסום שאינו 'כל יום'.

build (webpack) עובר, lint נקי. digests = hand-written types (אין api:types).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:14:23 +00:00
05e8373d22 Merge pull request 'feat(bulletins): catalog monthly "עו"ד על נדל"ן" bulletins into the radar (X12)' (#154) from worktree-bulletins-catalog into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m36s
2026-06-08 08:08:10 +00:00
85f94a4f3f feat(bulletins): catalog monthly "עו"ד על נדל"ן" bulletins into the radar (X12)
עלון חודשי רב-נושאי (פרסום נפרד מהיומון היומי) → מתפצל ל-N שורות digest באותה
טבלה (publication='עו"ד על נדל"ן', לא קורפוס מקביל — G2):
- bulletin_splitter (LLM local-only, tools=""): מפצל ל-cases[]+articles[];
  עדכוני-חקיקה מדולגים (החלטת יו"ר).
- bulletin_library.ingest_bulletin: כל מצביע-פסיקה → digest_kind='decision'
  + embedding + autolink (כולל X13 court-fetch); כל מאמר → digest_kind='article'
  (טקסט-מלא + embedding, רקע בלבד — INV-DIG1 חל).
- content_hash per-item הוא מפתח-הדדאפ (yomon_number ריק) → אידמפוטנטי.
- db.create_digest: פרמטר digest_kind (זורם ל-INSERT + upsert).
- scripts/ingest_bulletins.py (host, venv) לעיבוד הארכיון.
- spec X12 §2.1.

אומת (dry-run, ללא DB): עלון 180 → 4 cases+1 article · עלון 201 → 4 cases
(כולל ערר-197) +1 article. עדכוני-חקיקה דולגו. claude_session נשאר local-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:07:45 +00:00
1f42a39ce4 feat(storage): X14 Phase 2b — route extracted-text + async DOCX exports through storage.py
Continue the write-site rewiring onto the unified storage layer (INV-STG1):
- services/processor.py: extracted-text .txt → DERIVED bucket (a derived
  artifact; the DB column is the source of truth per INV-STG5, so the write
  stays non-fatal)
- services/docx_exporter.py (export_decision): DOCX → DOCUMENTS bucket via
  BytesIO → put_bytes, with a fallback to a direct disk write when the caller
  passes an output_path outside DATA_DIR
- services/analysis_docx_exporter.py (build_analysis_docx): same pattern;
  out_path is always under DATA_DIR

Under the default STORAGE_BACKEND=filesystem the bytes land at the exact
legacy path (put_bytes → DATA_DIR/key), so behaviour is unchanged. The
disk-reading bits that must stay for now (export_dir glob in _next_version)
are kept; storage-native versioning is a cutover concern.

Still on disk (sync call-sites, follow-up Phase 2c): docx_reviser
(track-changes), docx_retrofit backup, and multimodal thumbnails (rendered in
a to_thread). git-tracked text (case.json/notes/research-md/draft-md) stays on
disk by design (INV-STG7).

tests: 38 storage + docx tests green (incl. test_export_qa_gate /
test_docx_exporter_bookmarks which exercise the real export path); 242
collected, no import breakage.

Keeps G2; advances INV-STG1. Spec: docs/spec/X14-storage-minio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:05:25 +00:00
1986fe3b14 feat(storage): X14 Phase 2a — route source-document writes through storage.py
Rewire the source-document staging writes onto the unified storage layer
(INV-STG1), replacing direct shutil.copy2 calls:
- tools/documents.py: case originals + training-corpus uploads
- services/ingest.py: _stage_file (now async) — covers precedent-library,
  internal-decisions, and digests (the canonical intake helper)
- services/digest_library.py: awaits the now-async _stage_file

Each write goes through storage.put_file(..., bucket=DOCUMENTS) with the
DATA_DIR-relative key; the Hebrew original filename rides as object metadata
(INV-STG2), content-type is guessed from the extension. DB path columns are
unchanged (still the absolute dest) — object_key backfill is Phase 3.

Under the default STORAGE_BACKEND=filesystem the bytes land at the exact
legacy on-disk location (put_file → shutil.copy2 to DATA_DIR/key), so this
is zero behaviour change in prod. shutil import dropped where now unused.

tests: +2 staging regression tests (file lands under DATA_DIR at the legacy
path); 20 storage + 22 ingest tests green; 242 collected with no import
breakage.

Derived/export write sites (thumbnails, extracted text, DOCX exports) are
Phase 2b. Keeps G2; advances INV-STG1. Spec: docs/spec/X14-storage-minio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:00:27 +00:00
b4a28f072d feat(storage): X14 Phase 1 — unified storage layer (services/storage.py)
The single choke-point for all binary file I/O (originals, derived
artifacts, exports), replacing the scattered open()/shutil/Path.write_bytes
calls across ~8 services. Backend chosen by STORAGE_BACKEND:
- filesystem (default): disk under DATA_DIR — byte-for-byte legacy behaviour
- dual: write disk + S3, read S3→disk fallback (migration window)
- s3: MinIO via aioboto3 (lazy import; absent in the filesystem path)

Keys are DATA_DIR-relative POSIX paths; the FS backend ignores the logical
bucket and keeps the existing single tree, so the default backend is zero
behaviour change. S3 maps a governance bucket (documents/immutable/derived)
→ MinIO bucket; presigned URLs are minted against the public endpoint
(browser-reachable) and carry the Hebrew filename via RFC-5987
Content-Disposition.

- config: STORAGE_BACKEND + MINIO_* (endpoint, public-endpoint, creds,
  region, 3 bucket names, presign TTL)
- mcp_env_catalog: new "storage" category + 10 specs (X10/INV-ENV1)
- pyproject: aioboto3>=13 (consumed here, deployed with first use)
- tests: 18 unit tests (FS round-trip, key normalization/traversal guard,
  bucket resolution, backend selection, dual write-both + S3-down fallback)

No call-sites are rewired yet — that is Phase 2 (106.3). STORAGE_BACKEND
stays filesystem in prod, so behaviour is unchanged.

Invariants: keeps G2 (one storage path replaces scattered I/O); establishes
INV-STG1 (single layer), INV-STG2 (atomic keys, Hebrew name in metadata),
INV-STG3 (governance buckets), INV-STG6 (presigned serving).
Spec: docs/spec/X14-storage-minio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 07:47:49 +00:00