From 887079535c50b415627322f6a34c231533f6bb33 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 31 May 2026 18:42:13 +0000 Subject: [PATCH] feat(spec): X11 citation-corroboration + INV-G10 amendment + Opus 4.8 halacha extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ספ חדש לשכבת 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) --- .taskmaster/tasks/tasks.json | 2 +- docs/spec/00-constitution.md | 14 +- docs/spec/X11-citation-corroboration.md | 174 +++++++++++++++ mcp-server/src/legal_mcp/config.py | 13 ++ .../src/legal_mcp/services/claude_session.py | 18 +- .../legal_mcp/services/halacha_extractor.py | 7 +- scripts/SCRIPTS.md | 1 + scripts/ab_halacha_opus48.py | 202 ++++++++++++++++++ 8 files changed, 426 insertions(+), 5 deletions(-) create mode 100644 docs/spec/X11-citation-corroboration.md create mode 100644 scripts/ab_halacha_opus48.py diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 5f7ac3e..1ca4ee3 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -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": [ diff --git a/docs/spec/00-constitution.md b/docs/spec/00-constitution.md index b9b8056..e7bb3f2 100644 --- a/docs/spec/00-constitution.md +++ b/docs/spec/00-constitution.md @@ -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-COR1–COR6). **מקורות:** 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 | > **X6–X10 (מחזור-2):** מכסים את 8 משטחי-האפליקציה שמחוץ לצינור-הליבה (אינטגרציה, web-ui, מילוי-שדות, > אחסון-ניתוחים, כלי-MCP, deploy/env). הממצאים ב-[gap-audit.md](gap-audit.md) (GAP-24..62 → FU-9..15) diff --git a/docs/spec/X11-citation-corroboration.md b/docs/spec/X11-citation-corroboration.md new file mode 100644 index 0000000..c18202b --- /dev/null +++ b/docs/spec/X11-citation-corroboration.md @@ -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 עצמם מפספסים 23–25% מהטיפול — לכן נדרשת חזרתיות חוצת-מקורות. +**מקורות:** 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). diff --git a/mcp-server/src/legal_mcp/config.py b/mcp-server/src/legal_mcp/config.py index 990337b..c2e8c4b 100644 --- a/mcp-server/src/legal_mcp/config.py +++ b/mcp-server/src/legal_mcp/config.py @@ -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") diff --git a/mcp-server/src/legal_mcp/services/claude_session.py b/mcp-server/src/legal_mcp/services/claude_session.py index ced3ccc..c44916d 100644 --- a/mcp-server/src/legal_mcp/services/claude_session.py +++ b/mcp-server/src/legal_mcp/services/claude_session.py @@ -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) diff --git a/mcp-server/src/legal_mcp/services/halacha_extractor.py b/mcp-server/src/legal_mcp/services/halacha_extractor.py index 67c5523..a722fa7 100644 --- a/mcp-server/src/legal_mcp/services/halacha_extractor.py +++ b/mcp-server/src/legal_mcp/services/halacha_extractor.py @@ -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( diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 225dfa7..ba66e31 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -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 ...`. **חייב לרוץ מהמכונה המקומית** (לא קונטיינר) — `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__.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py `. **ממצא 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` (לתזמן) | diff --git a/scripts/ab_halacha_opus48.py b/scripts/ab_halacha_opus48.py new file mode 100644 index 0000000..7ae9c95 --- /dev/null +++ b/scripts/ab_halacha_opus48.py @@ -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 --effort ` 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 + +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 ", 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()) -- 2.49.1