feat(spec): X11 citation-corroboration + INV-G10 amendment + Opus 4.8 halacha extraction

ספ חדש לשכבת citator פנימית — תיקוף הלכות לפי טיפול-שיפוטי מצטבר (ציטוטים נכנסים),
לצמצום היקף האישור-הידני של היו"ר:

- docs/spec/X11-citation-corroboration.md — 6 invariants (INV-COR1–COR6), כל אחד עם
  ≥3 מקורות מקצועיים (Shepard's/KeyCite, Hellyer LLJ 2018, UNC Law, NCSC/JTC, CEPEJ).
- docs/spec/00-constitution.md — תיקון מבוקר ל-INV-G10: השער מסופק ע"י טיפול-שיפוטי-מצטבר
  לתת-הקבוצה החיובית, שער-היו"ר נשאר חובה לזנב ולשלילי. + X11 באינדקס.
- Opus 4.8 @ xhigh כמודל חילוץ הלכות (config HALACHA_EXTRACT_MODEL/EFFORT, env-tunable;
  claude_session model/effort params; halacha_extractor מחווט). מבוסס A/B 2026-05-31:
  פחות חילוץ-יתר, 100% quote-verified, ביטחון מכויל.
- scripts/ab_halacha_opus48.py — harness A/B לא-הרסני להשוואת מודל/effort בחילוץ הלכות.
- .taskmaster #70 (FU-2c-b) — תיעוד dedup שפר + סריקת-קורפוס (0 stubs תקועים נותרו).

תנאי-קדם (זהות נקייה) הושלם: שפר מוזג לרשומה קנונית + סריקת 128 רשומות.
audit-findings גלויים ב-X11 §7: קישור הלכה↔ציטוט + סיווג-טיפול = greenfield, ל-implementation plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 18:42:13 +00:00
parent d83a2a2fb2
commit 887079535c
8 changed files with 426 additions and 5 deletions

View File

@@ -2409,7 +2409,7 @@
"id": "70",
"title": "[FU-2c-b] תיאום + dedup של cited_only (49 רשומות) + אהוד שפר cross-source",
"description": "המשך ל-FU-2c (#68). ה-dry-run של תיאום-המזהים החיצוני חשף 49 רשומות source_kind='cited_only' (הפניות-ציטוט שחולצו מהחלטות) שלא היו בהיקף #68. דורשות נרמול נפרד: צורות-ועדה כמו 'ערר 1093-19' (NNNN-NN) שה-extractor הנוכחי לא תופס (NO_DOCKET), 'בש\"א 2487-14', dups, ו-'ערר אדלר' בלתי-פתיר (ללא מספר). בנוסף: dedup חוצה-source של אהוד שפר — external_upload 'עע\"מ 317/10 אהוד שפר' מול cited_only קיים 'עע\"מ 317/10' (אותו תיק; ה-collision-guard מנע התנגשות ב-uq_case_law_external_number, ה-external_upload נשאר עם case_number מנופח עד הכרעה).",
"details": "מקור: dry-run FU-2c 2026-05-31 (data/audit/fu2c-reconciliation-20260531T140632Z.{csv,md}). 73 רשומות <> internal_committee = 24 external_upload (טופלו ב-#68) + 49 cited_only. מתוך ה-cited_only: ~17 will_change (refs בצורת בית-משפט), 6 NO_DOCKET (ערר NNNN-NN + ערר אדלר), 5+ DUP_CHECK. דרוש: (1) הרחבת _DOCKET_RE לצורת-ועדה NNNN-NN; (2) הכרעה אם cited_only refs מקבלים נרמול מלא או נשארים כ-display; (3) dedup חוצה-source (cited_only שהפך ל-external_upload → מיזוג/הסרה, ראה precedent_link_cases/precedent_unlink_cases); (4) 'ערר אדלר' — סגירה ידנית. severity: Medium. סוג: data-migration + chair. הסקריפט scripts/fu2c_reconcile_external_case_numbers.py כבר מסנן apply ל-external_upload בלבד ומשאיר cited_only בשדה-ראייה לזיהוי-dup.",
"details": "מקור: dry-run FU-2c 2026-05-31 (data/audit/fu2c-reconciliation-20260531T140632Z.{csv,md}). 73 רשומות <> internal_committee = 24 external_upload (טופלו ב-#68) + 49 cited_only. מתוך ה-cited_only: ~17 will_change (refs בצורת בית-משפט), 6 NO_DOCKET (ערר NNNN-NN + ערר אדלר), 5+ DUP_CHECK. דרוש: (1) הרחבת _DOCKET_RE לצורת-ועדה NNNN-NN; (2) הכרעה אם cited_only refs מקבלים נרמול מלא או נשארים כ-display; (3) dedup חוצה-source (cited_only שהפך ל-external_upload → מיזוג/הסרה, ראה precedent_link_cases/precedent_unlink_cases); (4) 'ערר אדלר' — סגירה ידנית. severity: Medium. סוג: data-migration + chair. הסקריפט scripts/fu2c_reconcile_external_case_numbers.py כבר מסנן apply ל-external_upload בלבד ומשאיר cited_only בשדה-ראייה לזיהוי-dup. [עדכון 2026-05-31 — בוצע חלקית]: dedup חוצה-source של אהוד שפר הושלם — ה-stub cited_only 65a3a143 (עע״מ 317/10) מוזג ל-external_upload 9024da7b, 7 ציטוטים מופו-מחדש, case_number נורמל ל-עע״מ 317/10 (גיבוי data/audit/shafer-merge-backup.json). סריקת-קורפוס מלאה (128 רשומות): 0 stubs עם ציטוטים תקועים נותרו — כל 32 ה-cited_only עם ציטוטים לגיטימיים (אין רשומת-תוכן מקבילה). נמחקו 2 stubs ריקים מיותרים מעל/כפול תוכן: 1071-25 (3dce0689) ו-1009-02-24 (d05c771c) — גיבויים data/audit/stub-cleanup-*.json. נותר פתוח: (א) 1083-24 — ציטוט-משולב 'ערר (ירושלים) 1078+1083/24' (שני תיקים ב-stub אחד) → דורש טיפול combined-citation ב-citation_extractor (פיצול ל-1078-24 + 1083-24), לא מחיקת-נתונים; (ב) ~49 ה-cited_only הרחבים (_DOCKET_RE לצורת NNNN-NN, 'ערר אדלר') בהיקף המקורי.",
"testStrategy": "אחרי תיקון: 0 NO_DOCKET ב-cited_only (פרט ל-ערר אדלר המתועד); אין case_number כפול בין external_upload ל-cited_only; אהוד שפר עע\"מ 317/10 = רשומה אחת.",
"status": "pending",
"dependencies": [

View File

@@ -178,10 +178,19 @@ ISO 15489-1:2016 (records authenticity/integrity) | סטטוס: verified
### INV-G10: המערכת מסייעת — שערים אנושיים הם invariant
**כלל:** המערכת **מסייעת ואינה מחליפה את שיקול-הדעת האנושי**. השערים האנושיים (אישור
הלכה, בחירת תוצאה, פידבק היו"ר) הם **invariant — חובה, לא רשות**.
**תיקון (החלטת-יו"ר 2026-05-31):** שער אישור-ההלכה יכול להיות מסופק ע"י **טיפול שיפוטי מצטבר**
(citator פנימי), לא רק ע"י היו"ר — הלכה ש**אומצה (followed) ע"י ≥N ערכאות/ועדות מצטטות, ללא
טיפול שלילי**, מאושרת אוטומטית. זהו **שיפוט אנושי** (של המצטטים), לא שיפוט-AI (ה-AI רק מזהה
ומסווג את הטיפול הקיים). **שער-היו"ר נשאר חובה** לזנב הלא-מצוטט ולכל טיפול שלילי
(distinguished/overruled). מפורט ב-[X11-citation-corroboration.md](X11-citation-corroboration.md)
(INV-COR1COR6).
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* ("never replace human
judgment") · CEPEJ (2018, under user control) · Federal Judicial Center — *Judicial Writing
Manual* (2d ed.) | סטטוס: verified
**אכיפה:** שערים אנושיים בקוד-הזרימה (gate לא ניתן לעקיפה); מפורט ב-[05-qa-review.md](05-qa-review.md).
Manual* (2d ed.) · [לתיקון:] Shepard's / KeyCite citators + Hellyer, *Evaluating Shepard's,
KeyCite, and BCite* (Law Library Journal 110:4, 2018) — טיפול-שיפוטי-מצטבר כמתודולוגיה מוכרת
| סטטוס: verified
**אכיפה:** שערים אנושיים בקוד-הזרימה (gate לא ניתן לעקיפה); מסלול-corroboration ב-
[X11](X11-citation-corroboration.md); מפורט ב-[05-qa-review.md](05-qa-review.md).
**הפרה ידועה:** 10/19 הלכות מאושרות, התגלה במקרה — שער ידני שקוף בלי נראות backlog →
ממצא ל-[audit](../audit-report.md).
@@ -238,6 +247,7 @@ Manual* (2d ed.) | סטטוס: verified
| [X8-field-provenance.md](X8-field-provenance.md) | מקור-מילוי כל שדה (דטרמיניסטי/Opus/ידני/נגזר) · preservation · trust · verbatim-quote | G9, G10 |
| [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) | חוזה 71 כלי-ה-MCP: envelope · שמות · idempotency · extract/get-symmetry · שלמות-הרשאות | G2, G3, G10 |
| [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) | env-catalog SSoT · מקור-config יחיד (Coolify) · ללא hardcode · secrets · drift | G2, G4, G9 |
| [X11-citation-corroboration.md](X11-citation-corroboration.md) | citator פנימי — תיקוף הלכות בטיפול-שיפוטי מצטבר · תיקון-G10 מבוקר · סף-corroboration · התאמה-להלכה | G9, G10 |
> **X6X10 (מחזור-2):** מכסים את 8 משטחי-האפליקציה שמחוץ לצינור-הליבה (אינטגרציה, web-ui, מילוי-שדות,
> אחסון-ניתוחים, כלי-MCP, deploy/env). הממצאים ב-[gap-audit.md](gap-audit.md) (GAP-24..62 → FU-9..15)

View File

@@ -0,0 +1,174 @@
# X11 — תיקוף-הלכות בציטוטים (Citation Corroboration / Internal Citator)
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md). הוא מגדיר **שכבת citator פנימית**: שימוש
ב**ציטוטים-הנכנסים** לפסיקה (איך ערכאות וועדות מאוחרות *טיפלו* בה) כדי **לתקף ולחדד את ההלכות
שחולצו ממנה**, וכך לצמצם את היקף האישור-הידני של היו"ר. הוא אוכף את
[INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (כפי שתוקן —
ראה §6), נשען על [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
(עקיבוּת-מקור), ומעמיק את מודל-הציטוטים של [02-data-model.md](02-data-model.md).
> **TARGET, לא תיאור-מצב.** המנגנון כאן הוא היעד. רכיבים שטרם נבנו מסומנים מפורשות
> כ-audit-finding (§7), ולא כהתנהגות קיימת. כל טענה על הקוד מצוטטת `file:line`.
---
## 1. הרעיון — citator פנימי
בעולם המשפטי, הכלים שמאמתים פסיקה לפי הציטוטים-הנכנסים אליה הם **citators** (Shepard's של
LexisNexis, KeyCite של Westlaw, BCite של Bloomberg). הם עונים על שתי שאלות: *האם הפסק עדיין
"good law"?* ו-*איך ערכאות מאוחרות טיפלו בו?* — לפי **סיווג-טיפול** (treatment) של כל ציטוט-נכנס.
המערכת שלנו מחזיקה כבר את חומר-הגלם: גרף-ציטוטים פנימי (§2). מה שחסר הוא **השכבה שמחברת אותו
להלכות** — לתקף הלכה ספציפית לפי כך שערכאות/ועדות מאוחרות *אימצו* אותה בפועל. הלכה שאומצה
שוב-ושוב ע"י פאנלים אחרים אינה "ניחוש של מודל" — היא **טיפול שיפוטי אנושי מצטבר**, וזה הבסיס
שמאפשר אישור-אוטומטי בלי לפגוע בשיקול-הדעת האנושי (ראה תיקון INV-G10, §6).
---
## 2. חומר-הגלם הקיים — שני גרפי-ציטוט
| טבלה | קושר | הקשר נשמר | סיווג-טיפול |
|------|------|-----------|-------------|
| `case_law_citations` (`db.py:382`) | פסיקה ← **החלטת-ועדה פנימית** (`decisions`) | `context_text` | `citation_type` (support/distinguish/overrule/obiter) |
| `precedent_internal_citations` (`db.py:938`) | פסיקה ← **פסיקה אחרת** (`case_law`) | `match_context` | — (אין שדה-טיפול) |
**audit-finding (קיים):** ב-`precedent_internal_citations` **אין** שדה סיווג-טיפול, ו-ב-
`case_law_citations` שדה `citation_type` קיים אך **ברירת-המחדל `'support'`** (`db.py:387`) —
כלומר רוב הרשומות לא סווגו בפועל. סיווג-הטיפול הוא רכיב שיש לבנות (§4, INV-COR2).
---
## 3. תנאי-קדם — גרף-זהות נקי
ה-corroboration מצרף ציטוטים להלכות **דרך רשומת ה-`case_law`**. אם אותו תקדים מיוצג בשתי
רשומות (stub `cited_only` + רשומת-תוכן), הציטוטים יושבים על האחת וההלכות על האחרת — וה-join
נשבר. לכן **[INV-G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)/[INV-ID1](X1-identifiers.md)
הם תנאי-קדם קשיח** ל-X11.
**הפרה ידועה (תוקנה 2026-05-31):** אהוד שפר עע"מ 317/10 הוחזק בשתי רשומות — `external_upload`
עם ציטוט-מלא כ-`case_number` (הפרת INV-ID2) + `cited_only` stub שתפס את 7 הציטוטים-הנכנסים בנפרד
מ-53 ההלכות. מוזג לרשומה קנונית אחת; סריקת-קורפוס מלאה (128 רשומות) אישרה **0** stubs עם
ציטוטים-תקועים שנותרו. ראה [#70 / FU-2c-b](../audit-report.md). הניקוי השוטף של 49 ה-`cited_only`
(הרחבת `_DOCKET_RE`, ציטוטים-משולבים) ממשיך תחת #70.
---
## 4. המנגנון (TARGET)
```
לכל הלכה h של תקדים P:
1. אסוף ציטוטים-נכנסים ל-P (שני הגרפים, §2).
2. סווג טיפול לכל ציטוט (followed / distinguished / criticized / overruled / explained)
מתוך ההקשר (context_text / match_context) — Opus 4.8 @ xhigh. [INV-COR2]
3. התאם כל ציטוט להלכה הספציפית: דמיון סמנטי בין ההקשר לבין rule_statement של h,
מעל רף; הציטוט נספר ל-h רק אם הוא נוגע *לאותה הלכה*, לא לפסק כולו. [INV-COR3]
4. ספֵר corroboration של h = מספר ציטוטים חיוביים בלתי-תלויים שהותאמו אליה.
5. אישור:
אם ≥N חיוביים בלתי-תלויים ∧ 0 שליליים → אישור-אוטומטי (corroborated). [INV-COR4]
אם יש טיפול שלילי (distinguished/criticized/overruled) → אסור אוטו;
דגל ליו"ר, ואף הדחה אם overruled. [INV-COR2]
אחרת (לא-מצוטט) → נשאר בשער-היו"ר הרגיל (סף-confidence). [INV-COR5]
6. העשרה (משני): נסח-מחדש/חדד את rule_statement לפי המסגור של הפאנל המצטט.
```
**N (סף-corroboration)** ייקבע אמפירית (≥2 ברירת-מחדל; ציטוט יחיד אינו מספיק — INV-COR4).
---
## 5. Invariants של התחום
### INV-COR1: corroboration = טיפול שיפוטי אנושי מצטבר, לא שיפוט-AI
**כלל:** אישור-הלכה מבוסס-ציטוט נשען על כך ש**ערכאות/ועדות אנושיות אימצו את ההלכה בפועל** —
לא על ציון-ביטחון של מודל. ה-AI רק **מזהה ומסווג** את הטיפול הקיים; ההכרעה הערכית שההלכה
תקפה ניתנה ע"י השופטים המצטטים. זהו הבסיס לתיקון INV-G10 (§6).
**מקורות:** Shepard's Citations (LexisNexis) — citator + treatment analysis · KeyCite (Westlaw)
— good-law/treatment flags · Hellyer, *Evaluating Shepard's, KeyCite, and BCite* (Law Library
Journal 110:4, 2018) | סטטוס: verified
**אכיפה:** מנגנון §4 — corroboration נספר רק מטיפול שיפוטי מתועד, לא מ-confidence.
**הפרה ידועה:**
### INV-COR2: סיווג-טיפול חובה לפני ספירה — שלילי לעולם לא מאשר
**כלל:** כל ציטוט-נכנס מסווג ל**טיפול** (followed/explained = חיובי-נייטרלי;
distinguished/criticized/questioned/overruled = שלילי) לפני שהוא נספר. **טיפול שלילי לעולם אינו
תורם ל-corroboration ואינו מאשר אוטומטית**; overruled → הדחת ההלכה לבדיקת-יו"ר.
**מקורות:** Shepard's editorial treatment phrases (human-assigned depth-of-treatment) · KeyCite
flag system (red/yellow/green) · UNC Law, *Describing Negative Legal Precedent in Citators*
(Faculty Publications) | סטטוס: verified
**אכיפה:** שלב 2+5 ב-§4; סכֵמת-טיפול ב-`precedent_internal_citations` (שדה חדש) +
`case_law_citations.citation_type` (לא להישען על ברירת-המחדל `'support'`).
**הפרה ידועה:** סיווג-טיפול לא קיים בפועל (§2) — רכיב לבנייה.
### INV-COR3: התאמה להלכה הספציפית — לא לפסק כולו
**כלל:** ציטוט נספר ל-corroboration של הלכה h **רק אם ההקשר המצטט נוגע לאותה הלכה** (דמיון
סמנטי מעל רף). פסק מצוטט לעניין A אינו מתקף הלכה B שחולצה מאותו פסק.
**מקורות:** Hellyer (2018) — *"a 'followed' tag might refer to a different legal point than the
one you care about"* · UChicago Library, *Citators* research guide (treatment ≠ point-specific) ·
Northwestern Pritzker, *Determining Whether Cases Are Still Good Law* | סטטוס: verified
**אכיפה:** שלב 3 ב-§4 — רף-דמיון סמנטי בין ההקשר ל-rule_statement; Opus 4.8 כשופט-התאמה.
**הפרה ידועה:**
### INV-COR4: סף ≥N ציטוטים בלתי-תלויים — ציטוט יחיד אינו מספיק
**כלל:** אישור-אוטומטי דורש **≥N ציטוטים חיוביים בלתי-תלויים** — כלומר מ-**מקורות-מצטטים
מובחנים** (החלטות/פסקים שונים; שני אזכורים באותה החלטה = ציטוט אחד). ברירת-מחדל N=2. מקור יחיד
אינו ראיה מספקת; citators עצמם מפספסים 2325% מהטיפול — לכן נדרשת חזרתיות חוצת-מקורות.
**מקורות:** Hellyer (2018) — citator coverage gaps (Shepard's miss 23%, KeyCite 25%) · Manning,
Raghavan & Schütze, *Introduction to Information Retrieval* (CUP 2008) — aggregation of weak
signals · KeyCite/Shepard's depth-of-treatment (multiple citing refs) | סטטוס: verified
**אכיפה:** שלב 4-5 ב-§4; `HALACHA_CORROBORATION_MIN_CITES` (env-tunable, ברירת-מחדל 2).
**הפרה ידועה:**
### INV-COR5: השער האנושי נשמר לזנב הלא-מצוטט ולשלילי
**כלל:** corroboration **מצמצם** את היקף האישור-הידני; הוא **אינו מבטל** את שער-היו"ר. הלכות
לא-מצוטטות, וכל הלכה עם טיפול שלילי, **נשארות בשער-היו"ר**. גם ה-citators המקצועיים קובעים
ש"human review remains essential".
**מקורות:** Hellyer (2018) — *"There's no substitute for reading the actual citing case"* ·
NCSC/JTC, *Principles & Practices for AI Use in Courts* (human-in-the-loop) · CEPEJ (2018,
user-control) | סטטוס: verified
**אכיפה:** שלב 5 ב-§4; שער-היו"ר הקיים ([05-qa-review.md](05-qa-review.md)) נשאר על הזנב.
**הפרה ידועה:**
### INV-COR6: עקיבוּת — כל אישור-אוטומטי שומר את ראיית-הציטוט
**כלל:** הלכה שאושרה ב-corroboration **שומרת את הציטוטים המתקפים** (מזהי-המקור + ההקשר +
הטיפול) כ-provenance הניתן לביקורת — מי אישר, על סמך אילו פסקים, ובאיזה טיפול.
**מקורות:** [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) · ISO 15489-1:2016
(records authenticity) · CEPEJ (2018, transparency) | סטטוס: verified (נגזר מ-G9)
**אכיפה:** `halachot.reviewer` = `corroborated (≥N judicial citations)` + טבלת-קישור
הלכה↔ציטוטים-מתקפים; מוצג ביו"ר-UI.
**הפרה ידועה:**
---
## 6. תיקון INV-G10 (מבוקר)
INV-G10 קובע ששער אישור-ההלכה הוא invariant אנושי-חובה. **התיקון** (החלטת-יו"ר 2026-05-31)
אינו מבטל את השער אלא **מרחיב את מקור-הסמכות האנושית שלו**: השער מסופק ע"י **טיפול שיפוטי
מצטבר** (ערכאות/ועדות מצטטות) עבור תת-הקבוצה ה-corroborated החיובית, בעוד **שער-היו"ר נשאר חובה**
לזנב הלא-מצוטט ולכל טיפול-שלילי. הנוסח המתוקן + המקורות נכתבים ב-
[00-constitution.md INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant).
עיקרון-העל (INV-COR1) שומר על רוח G10: זהו שיפוט אנושי (של המצטטים), לא שיפוט-AI.
---
## 7. מצב קיים מול יעד — audit-findings
- **קישור הלכה↔ציטוט לא קיים.** אין טבלה/שאילתה שמצרפת ציטוט-נכנס להלכה ספציפית — רכיב-ליבה
לבנייה (§4 שלב 3).
- **סיווג-טיפול חסר.** `precedent_internal_citations` ללא שדה-טיפול; `case_law_citations.citation_type`
על ברירת-מחדל `'support'` (`db.py:387`) — לא מסווג בפועל (§2, INV-COR2).
- **אישור-אוטומטי כיום מבוסס-confidence בלבד.** `db.store_halachot` מאשר ב-`confidence ≥
HALACHA_AUTO_APPROVE_THRESHOLD` (`db.py:3221`, ברירת-מחדל 0.80) — לא מבוסס-ציטוט. X11 מוסיף
מסלול-אישור שני (corroboration) לצד/מעל סף-ה-confidence.
- **גרף-זהות.** תוקן לשפר + dedup content-affecting (§3); המשך ניקוי ב-#70.
---
## 8. הפניות-אחיות
- [00-constitution.md](00-constitution.md) — INV-G9 (provenance), INV-G10 (שער אנושי, מתוקן §6),
פרוטוקול ≥3-מקורות.
- [02-data-model.md](02-data-model.md) — טבלות הציטוטים (`case_law_citations`,
`precedent_internal_citations`) + ישות `halachot`.
- [05-qa-review.md](05-qa-review.md) — שער אישור-ההלכה הקיים (נשאר על הזנב, INV-COR5).
- [07-learning.md](07-learning.md) — צמיחת-קורפוס + לולאת-הלכות.
- [X1-identifiers.md](X1-identifiers.md) — תנאי-הקדם: זהות קנונית (INV-ID1/ID2).
- [#70 / FU-2c-b](../audit-report.md) — dedup של `cited_only` (תנאי-קדם, §3).

View File

@@ -42,6 +42,19 @@ POSTGRES_URL = os.environ.get(
# Redis
REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6380/0")
# Claude CLI — model + effort for halacha extraction.
# All LLM calls go through the local `claude -p` CLI (claude_session.py).
# By default the CLI uses the developer's session default model with no
# explicit effort. For halacha extraction we pin Opus 4.8 @ xhigh: the
# 2026-05-31 A/B (scripts/ab_halacha_opus48.py) showed it cuts over-extraction
# (~124→51 on שטיין) at 100% quote-verification with honest confidence
# calibration. Env-overridable so the model/effort can be tuned without a
# code change (set to "" to fall back to the CLI default). Other extractors
# (claims, metadata, block-writing, QA) keep the CLI default unless similarly
# pinned.
HALACHA_EXTRACT_MODEL = os.environ.get("HALACHA_EXTRACT_MODEL", "claude-opus-4-8")
HALACHA_EXTRACT_EFFORT = os.environ.get("HALACHA_EXTRACT_EFFORT", "xhigh")
# Voyage AI
VOYAGE_API_KEY = os.environ.get("VOYAGE_API_KEY", "")
VOYAGE_MODEL = os.environ.get("VOYAGE_MODEL", "voyage-law-2")

View File

@@ -47,6 +47,8 @@ async def query(
max_turns: int = 1,
*,
system: str | None = None,
model: str | None = None,
effort: str | None = None,
) -> str:
"""Send a prompt to Claude Code headless and return the text response.
@@ -62,6 +64,13 @@ async def query(
CLI doesn't expose API-level caching. The parameter exists so
extractors can structure their calls cleanly today, and to make
a future SDK-backed path drop-in.
model: Optional model alias/id (e.g. ``claude-opus-4-8``). When set,
passed as ``--model``; otherwise the CLI's session default is
used. Lets quality-sensitive extractors (halacha) pin a stronger
model without changing the default for every caller.
effort: Optional effort level (``low``/``medium``/``high``/``xhigh``/
``max``). When set, passed as ``--effort``. Pairs with ``model``;
an empty string is treated as "unset" (CLI default).
Returns:
The text response from Claude.
@@ -80,6 +89,10 @@ async def query(
"--output-format", "json",
"--max-turns", str(max_turns),
]
if model:
cmd += ["--model", model]
if effort:
cmd += ["--effort", effort]
try:
proc = await asyncio.create_subprocess_exec(
@@ -135,12 +148,15 @@ async def query_json(
timeout: int = DEFAULT_TIMEOUT,
*,
system: str | None = None,
model: str | None = None,
effort: str | None = None,
) -> dict | list | None:
"""Send a prompt and parse the response as JSON.
Uses parse_llm_json for robust parsing (handles markdown wrapping, truncation).
``model``/``effort`` are forwarded to :func:`query` (see its docstring).
"""
raw = await query(prompt, timeout=timeout, system=system)
raw = await query(prompt, timeout=timeout, system=system, model=model, effort=effort)
return parse_llm_json(raw)

View File

@@ -304,7 +304,12 @@ async def _extract_chunk(
last_err: Exception | None = None
for attempt in range(CHUNK_RETRY_ATTEMPTS + 1):
try:
result = await claude_session.query_json(user_msg, system=base_prompt)
result = await claude_session.query_json(
user_msg,
system=base_prompt,
model=config.HALACHA_EXTRACT_MODEL or None,
effort=config.HALACHA_EXTRACT_EFFORT or None,
)
except Exception as e:
last_err = e
logger.warning(

View File

@@ -37,6 +37,7 @@
| `backfill_legal_arguments.py` | python | Backfill `legal_arguments` לתיקים עם `claims` קיימים (TaskMaster #36). מקבץ פרופוזיציות גולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד) דרך `argument_aggregator.aggregate_claims_to_arguments` (Claude CLI). תומך `--dry-run`/`--apply`/`--force`/`--case <num>...`. **חייב לרוץ מהמכונה המקומית** (לא קונטיינר) — `claude_session` דורש Claude CLI | ידני per-case (`python scripts/backfill_legal_arguments.py --apply --case 1017-03-26`) |
| `upload_blam_decisions.py` | python | חד-פעמי (2026-05-26) — העלאת 2 החלטות בל"מ ל-`case_law` (8126/24 סופר נוח, 8047/23 הרנון) דרך `ingest_internal_decision` ישיר, עוקף MCP server שטרם נטען מחדש אחרי הוספת `proceeding_type`. **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
| `process_pending_blam.py` | python | חד-פעמי (2026-05-26) — הרצת metadata + halacha extraction על 2 החלטות בל"מ שעלו ב-`upload_blam_decisions.py`. עוקף MCP (אותו טעם). **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
| `ab_halacha_opus48.py` | python | **A/B לא-הרסני לחילוץ הלכות** — מריץ מחדש חילוץ הלכות על פסק-דין בודד דרך מודל/effort נבחרים (`AB_MODEL`/`AB_EFFORT`, ברירת-מחדל `claude-opus-4-8`/`xhigh`) ומשווה לסטטיסטיקות ההלכות הקיימות ב-DB **בלי למחוק/לכתוב כלום**. משכפל את `halacha_extractor.extract()` (אותם פרומפטים, בחירת-צ'אנקים, אימות-ציטוט) ומחליף רק את קריאת ה-LLM ב-`claude -p --model --effort`. מפיק `data/ab_halacha_<case>_<effort>.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py <case_law_id>`. **ממצא 2026-05-31 (שטיין 1128-08-20):** Opus 4.8@xhigh חילץ 51 מול 124 בייצור (100% quote-verified מול 96%) אך ביטחון מכויל-נמוך יותר (חציון 0.75 מול 0.82) — ולכן **לא** מקטין את תור-האישור-הידני תחת sweep אוטו-אישור conf≥0.78 (26 מול 24). שיפור איכות, לא צמצום-תור. | ידני (החלטת מודל-חילוץ) |
| `compute_ndcg.py` | python | חישוב nDCG@10 על `search_relevance_feedback` (TaskMaster #50, Stage C). aggregation לפי `search_type` ולפי שבוע, כולל top-cited case_law ו-coverage %. דגלים: `--k 10`, `--weeks 12`, `--pretty`. read-only, פלט JSON. משמש גם את `GET /api/admin/rag-metrics` (מיובא inline) — שינוי חתימה ב-`compute()` ישבור את ה-endpoint | ידני / cron עתידי לדיווח שבועי |
| `backfill_multimodal_precedents.py` | python | Backfill voyage-multimodal-3 page embeddings על רשומות `case_law` (external_upload + internal_committee) שחסרות `precedent_image_embeddings`. בונה אינדקס קבצים מ-`data/precedent-library/` ו-`data/internal-decisions/`, מנסה התאמה לפי tokens של מספרי תיק (כולל parts-match לפורמטים שונים של Nevo doc-id). מדלג על רשומות בלי קובץ-מקור או עם MD בלבד (PyMuPDF לא מרנדר MD). תומך `--dry-run` (default) / `--apply` / `--only external_upload\|internal_committee` / `--limit N`. רץ בקונטיינר (יש `/data` + Voyage env). **הופעל 2026-05-26**: 70 חסרים → 26 backfilled (503 pages, ~$0.21 voyage tokens), 44 אין-קובץ-מקור. ניתן להריץ שוב אחרי שיועלו עוד PDF/DOCX לספרייה | ידני |
| `monitor_halacha_quality.py` | python | מנטר איכות חילוץ הלכות. בודק drift של `avg(confidence)` בין baseline היסטורי לחלון אחרון. מחזיר JSON מטריקות + alert ב-stderr אם drift > threshold (ברירת מחדל 5%). 2 סדרות: trusted (approved+published) ו-all_extracted. תומך `--window N` / `--threshold X` / `--min-sample N` / `--silent` / `--exit-on-alert`. רץ ב-container או מקומית עם `mcp-server/.venv` (אין תלות ב-LLM, רק SQL). **תזמון מומלץ**: `0 8 * * 1` (יום ראשון 08:00, שבועי) | `0 8 * * 1` (לתזמן) |

View File

@@ -0,0 +1,202 @@
#!/usr/bin/env python
"""A/B (NON-DESTRUCTIVE): re-extract halachot for ONE precedent with a chosen
model+effort and compare against the existing stored halachot.
Purpose: decide whether re-running halacha extraction on Opus 4.8 (@ xhigh/max
effort) yields fewer / higher-quality halachot than the current production
output — WITHOUT deleting or storing anything in the DB.
Mirrors the production pipeline in `halacha_extractor.extract()` (same prompts,
same chunk selection + fallback, same quote-verification), but swaps the LLM
call for `claude -p --model <M> --effort <E>` and skips embeddings + DB writes.
Usage:
DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \
AB_MODEL=claude-opus-4-8 AB_EFFORT=xhigh \
.venv/bin/python scripts/ab_halacha_opus48.py <case_law_id>
Env knobs: AB_MODEL (default claude-opus-4-8), AB_EFFORT (default xhigh),
AB_CONCURRENCY (default 2).
"""
from __future__ import annotations
import asyncio
import json
import os
import statistics
import sys
from collections import Counter
from uuid import UUID
from legal_mcp.config import parse_llm_json
from legal_mcp.services import db
from legal_mcp.services import halacha_extractor as hx
MODEL = os.environ.get("AB_MODEL", "claude-opus-4-8")
EFFORT = os.environ.get("AB_EFFORT", "xhigh")
CONCURRENCY = int(os.environ.get("AB_CONCURRENCY", "2"))
CHUNK_TIMEOUT = int(os.environ.get("AB_CHUNK_TIMEOUT", "1800"))
async def run_claude(system: str, prompt: str, timeout: int = CHUNK_TIMEOUT):
"""One `claude -p` call with explicit --model/--effort. Returns parsed JSON."""
full = f"{system}\n\n{prompt}"
cmd = [
"claude", "-p", "--output-format", "json", "--max-turns", "1",
"--model", MODEL, "--effort", EFFORT,
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
out_b, err_b = await asyncio.wait_for(
proc.communicate(input=full.encode("utf-8")), timeout=timeout,
)
if proc.returncode != 0:
raise RuntimeError(
f"claude CLI exit {proc.returncode}: "
f"{err_b.decode('utf-8', 'replace').strip()[:300]}"
)
raw = out_b.decode("utf-8", "replace").strip()
try:
data = json.loads(raw)
if isinstance(data, dict) and "result" in data:
raw = data["result"]
except json.JSONDecodeError:
pass
return parse_llm_json(raw)
async def extract_chunk(chunk_text, section_type, idx, total, context, is_binding):
base_prompt = (
hx.HALACHA_EXTRACTION_PROMPT_BINDING if is_binding
else hx.HALACHA_EXTRACTION_PROMPT_PERSUASIVE
)
chunk_label = f" (חלק {idx + 1}/{total})" if total > 1 else ""
user_msg = (
f"## הקלט\n"
f"סוג קטע: {section_type}\n"
f"{context}{chunk_label}\n\n"
f"--- תחילת הטקסט ---\n{chunk_text}\n--- סוף הטקסט ---"
)
try:
result = await run_claude(base_prompt, user_msg)
except Exception as e:
print(f" ! chunk {idx + 1}/{total} failed: {e}", file=sys.stderr)
return [], False
if isinstance(result, list):
return result, True
print(f" ! chunk {idx + 1}/{total} non-list: {type(result).__name__}", file=sys.stderr)
return [], False
def stats(halachot: list[dict], label: str) -> dict:
n = len(halachot)
def fconf(x):
try:
return float(x.get("confidence"))
except (TypeError, ValueError):
return None
confs = [c for c in (fconf(h) for h in halachot) if c is not None]
qv = Counter(bool(h.get("quote_verified")) for h in halachot)
rt = Counter(h.get("rule_type") for h in halachot)
return {
"label": label, "n": n,
"quote_verified_true": qv.get(True, 0),
"quote_verified_false": qv.get(False, 0),
"conf_min": min(confs) if confs else None,
"conf_median": statistics.median(confs) if confs else None,
"conf_max": max(confs) if confs else None,
"conf_below_0_7": sum(1 for c in confs if c < 0.7),
"rule_types": dict(rt),
}
def print_stats(s: dict):
print(f"\n=== {s['label']} ===")
print(f" count : {s['n']}")
print(f" quote_verified : {s['quote_verified_true']} ✓ / {s['quote_verified_false']}")
if s["conf_median"] is not None:
print(f" confidence min/med/max: {s['conf_min']:.2f} / {s['conf_median']:.2f} / {s['conf_max']:.2f}")
print(f" confidence < 0.7 : {s['conf_below_0_7']} / {s['n']}")
print(f" rule_type dist : {s['rule_types']}")
async def main():
if len(sys.argv) < 2:
print("usage: ab_halacha_opus48.py <case_law_id>", file=sys.stderr)
sys.exit(2)
case_law_id = UUID(sys.argv[1])
record = await db.get_case_law(case_law_id)
if not record:
print("case_law not found", file=sys.stderr)
sys.exit(1)
is_binding = bool(record.get("is_binding"))
citation = record.get("case_number", "")
court = record.get("court", "")
date_str = str(record.get("date") or "")
full_text = record.get("full_text") or ""
print(f"Precedent: {citation}{record.get('case_name')}")
print(f" court={court} is_binding={is_binding} prompt={'BINDING' if is_binding else 'PERSUASIVE'}")
print(f" model={MODEL} effort={EFFORT} concurrency={CONCURRENCY}")
# ---- Side A: existing stored halachot (current production output) ----
existing = await db.list_halachot(case_law_id=case_law_id, limit=500)
by_status = Counter(h.get("review_status") for h in existing)
print(f"\n[A] existing halachot in DB: {len(existing)} status breakdown: {dict(by_status)}")
approved = by_status.get("approved", 0) + by_status.get("published", 0)
if approved:
print(f"{approved} already approved/published — a REAL re-run would DELETE these.")
# ---- Side B: fresh extraction via chosen model/effort (no DB writes) ----
chunks = await db.list_precedent_chunks(case_law_id, section_types=hx.EXTRACTABLE_SECTIONS)
if not chunks:
chunks = await db.list_precedent_chunks(case_law_id)
print(f"\n[B] extracting from {len(chunks)} chunks via {MODEL} @ {EFFORT} ...")
context = f"מקור: {citation}{court}, {date_str}"
sem = asyncio.Semaphore(CONCURRENCY)
async def bounded(i, c):
async with sem:
return await extract_chunk(c["content"], c["section_type"], i, len(chunks), context, is_binding)
results = await asyncio.gather(*[bounded(i, c) for i, c in enumerate(chunks)])
raw_b, failed = [], 0
for items, ok in results:
raw_b.extend(items)
if not ok:
failed += 1
cleaned_b = []
for raw in raw_b:
coerced = hx._coerce_halacha(raw, is_binding=is_binding)
if coerced is None:
continue
coerced["quote_verified"] = hx._verify_quote(coerced["supporting_quote"], full_text)
cleaned_b.append(coerced)
print(f" raw={len(raw_b)} valid={len(cleaned_b)} failed_chunks={failed}/{len(chunks)}")
# ---- Comparison ----
a_stats = stats(existing, f"A · current production (n={len(existing)})")
b_stats = stats(cleaned_b, f"B · {MODEL} @ {EFFORT}")
print_stats(a_stats)
print_stats(b_stats)
# Dump B halachot for human quality judgement
out_path = f"/home/chaim/legal-ai/data/ab_halacha_{citation.replace('/', '_').replace(chr(34), '').strip()}_{EFFORT}.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(
{"precedent": citation, "model": MODEL, "effort": EFFORT,
"A_stats": a_stats, "B_stats": b_stats,
"B_halachot": cleaned_b}, f, ensure_ascii=False, indent=2,
)
print(f"\nB halachot written to: {out_path}")
if __name__ == "__main__":
asyncio.run(main())