Commit Graph

415 Commits

Author SHA1 Message Date
6c870ac691 perf(citation-view): cache מבוסס-טביעה — 24.6 שנ' לטעינה חוזרת → 0.01 שנ'
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 5s
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
הרכבת תצוגת אימות-הפסיקה עולה 20+ שניות של embeds וחיפושים וקטוריים,
והתוצאה משתנה רק כשמשתנים הקלטים שלה. עד עכשיו כל פתיחת-דף שילמה את
המחיר המלא מחדש, וכל רענון תוך-כדי-טעינה הוסיף הרצה מקבילה — כך העומס
המקורי התגלגל ל-deadlocks ב-Postgres.

למה טביעה ולא TTL: יו"ר שמאמת תקדים חייב לראות זאת בטעינה **הבאה**, לא
כשיפוג טיימר. `_inputs_fingerprint` הוא שאילתה אחת זולה שמסכמת בדיוק את
מה שהתצוגה נגזרת ממנו — טיעונים · שיוכי-פסיקה · גודל-הקורפוס. השתנה
משהו → הטביעה זזה → נבנה מחדש. **הנכונות אינה תלויה בכך שמישהו יזכור
לבטל** (G1): הבדיקה יושבת במקור-האמת ולא בזיכרון של כל כותב. `invalidate()`
קיים לנוחות, לא לתקינות.

מה שונה
- cache פר-תיק, מפתח = טביעת-הקלטים. תצוגה שלמה אינה פגה בטיימר.
- **מנעול פר-תיק נגד היצף** — שש טעינות מקבילות מריצות הרכבה אחת. זה
  בדיוק התרחיש שהפיל את המערכת.
- תצוגה **חלקית** (תקציב-האחזור נגמר) נשמרת ל-90 שנ' בלבד: מספיק כדי
  לעצור סופת-רענונים, לא מספיק כדי שההצעות החסרות ייתקעו לנצח.
- `cached: true/false` בתשובה, ו-`use_cache=False` למי שצריך רענון כפוי.

מדידה מול הקורפוס החי (8124-09-24):
    טעינה קרה ........ 24.6 שנ'   cached=false
    טעינה חוזרת ...... 0.01 שנ'   cached=true      ← פי 2,074
    5 מקבילות ........ 0.02 שנ'   כולן מה-cache
    אחרי invalidate .. 24.8 שנ'   cached=false

invariants: G1 (השער במקור-האמת, לא בכל כותב) · §6 (חלקי מסומן ופג,
לא נשמר כשלם)

טסטים: 6 חדשים (tests/test_citation_view_cache.py) — כולל היצף-מקבילי,
פקיעת תצוגה חלקית, וביטול-אוטומטי בלי קריאה מפורשת ל-invalidate.
537 עוברים.
2026-08-05 13:35:39 +00:00
5a2a989e9b fix(retrieval): סף מכויל-קוסינוס סינן פלט RRF — דף אימות-הפסיקה הציג אפס תקדימים
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 5s
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
`hybrid_search._merge_sem_lex` דורס את `score` בערך RRF (~0.008–0.02) ברגע
שה-leg הלקסיקלי מחזיר שורות. `score` הוא אות-דירוג לגיטימי, אבל הוא מפסיק
להיות קוסינוס — ואילו `case_citation_verification._SUGGEST_FLOOR = 0.45`
כויל לקוסינוס. התוצאה: **כל שאילתה במונחים משפטיים נפוצים סוננה עד
האחרונה**, והדף הציג "אין תקדים תומך" לכל טיעון בכל תיק.

אותה פונקציה החזירה שתי סקאלות, תלוי בשאילתה:

    "המונח חזית הבניין יפורש..."   leg לקסיקלי ריק   →  0.63–0.71  (קוסינוס)
    "סמכות ועדה מקומית לפי 62א"    leg לקסיקלי מלא   →  0.016      (RRF)

מה שונה
- **עוגן קוסינוס במקור (G1):** `db.search_precedent_library_semantic` מסמן
  `relevance` לצד `score`. **לא** בפונקציה הלקסיקלית — שם `ts_rank_cd`,
  ולסמן אותו כ-relevance היה חוזר על אותה טעות בדיוק.
- **הפיוז'ן משמר:** `_merge_sem_lex` מעביר את הקוסינוס הלאה. שורה
  לקסיקלית-בלבד מקבלת `relevance = None` — "לא נמדד" אינו "נמדד כלא-רלוונטי".
- **הצרכן קורא `relevance`:** `_passes_floor()` במקום השוואה ל-`score`.
  שורות לקסיקליות-בלבד **נשמרות במודע** — הן הגיעו לצמרת בדירוג BM25 בתוך
  top-k זעיר, וסינונן היה מחביא בדיוק את התאמות-הביטוי ומספרי-התיק שהיו"ר
  מחפש בשם.

הדירוג לא השתנה: RRF ממשיך לקבוע סדר. רק הסינון עבר לסקאלה יציבה.

אימות מול הקורפוס החי:
  8124-09-24:  0 → **32 מתוך 32** טיעונים עם תקדים תומך (21.5 שנ', שלם)
  1069-04-26:  0 → **29 מתוך 69** (24.1 שנ', חלקי — תקציב הזמן)
  שורה סמנטית: score=0.0082 · relevance=0.7297

היומונים לא נפגעו ולא נגעתי בהם: `case_digest_radar` עובר דרך
`search_digests_semantic` — סמנטי טהור, בלי RRF, ולכן `min_score=0.45` שלו
מכויל נכון.

invariants: G1 (עוגן במקור, לא תיקון-סף בקריאה) · G2 (הגדרה אחת ל-relevance
לכל הצרכנים) · INV-AH (היעדר-מדידה אינו היעדר-רלוונטיות)

טסטים: 6 חדשים (tests/test_relevance_scale.py) — אחד מהם מוכיח את הבאג
ואת התיקון באותה שורה. 531 עוברים.
2026-08-05 13:31:12 +00:00
d65c335a4a fix(citation-view): פיזור בלתי-מוגבל הפיל את דף אימות-הפסיקה ב-500
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 41s
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
פתיחת טאב "אימות פסיקה" ירתה `asyncio.gather` על **כל** טיעוני התיק בבת
אחת — טיעון אחד = קריאת embed ל-Voyage + חיפוש היברידי + שאילתת סמכות.
מעל ~8 קריאות במקביל Voyage מווסת, וכולן נתקעות יחד. הפיזור הבלתי-מוגבל
היה לא רק מסוכן אלא **איטי יותר**:

    חיפוש בודד ............  1.2 שנ'
    32 ללא מגבלה .......... 30.4 שנ'   ← פי 25 מחיפוש בודד
    32 עם מגבלת 8 ......... 22.1 שנ'   ← מבוקר = מהיר יותר

תיק עם 69 טיעונים (1069-04-26) חרג מ-timeout ה-30 שניות של הפרוקסי והחזיר
500; רענוני-דף חופפים הערימו תחרות עד `deadlock detected` ב-Postgres,
והאטו את כל המערכת.

מה שונה
- `_MAX_CONCURRENT_LOOKUPS = 8` — semaphore על הפיזור.
- `_RETRIEVAL_BUDGET_S = 22` — תקציב זמן שנגמר לפני הפרוקסי.
- בפקיעת התקציב **קוצרים את מה שהספיק** במקום לבטל הכל. במפורש *לא*
  `wait_for(gather(...))`: הוא מבטל כל משימה, כך שאיחור אחד היה זורק 30
  תוצאות שכבר הצליחו. משימות ממתינות מבוטלות ונאספות לפני שהמאגר
  מתפרק, אחרת נותרת `connection_lost` יתומה.
- `retrieval_complete: false` בתשובה — הצעות חסרות מסומנות ולא מוצגות
  כ"אין תקדים תומך" (§6). שתי הטענות שונות.

מדידה אחרי: 24 מתוך 32 טיעונים ו-26 מתוך 69 חוזרים עם הצעות, במקום 500.

invariants: §6 (חלקי מסומן, לא נבלע) · INV-AH (לא להציג היעדר-מידע
כהיעדר-תקדים)

⚠️ **נותר פתוח, נפרד:** גם כשהחיפוש מצליח `arguments_with_support=0` —
`hybrid_search.py:305` דורס את ציון הקוסינוס בציון RRF כשה-leg הלקסיקלי
מחזיר תוצאות, כך ש-`_SUGGEST_FLOOR=0.45` (מכויל לקוסינוס) מסנן הכל.
מטופל ב-PR נפרד.

טסטים: 4 חדשים (tests/test_citation_view_fanout.py) — הראשונים נועלים את
המגבלה והתקציב, האחרון משחזר בדיוק את הרגרסיה: קציר מול ביטול-הכל.
525 עוברים.
2026-08-05 13:25:41 +00:00
63387c0d7d fix(extraction): מספר-תיק שהומצא נכתב לשדה-הזהות — עיגון בטקסט לפני כתיבה (#232 מלכודת 3)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
ערר (מרכז) 1094-09-19 (פדילה) נשמר כ-`1094-09-14` בעוד כל חמשת מסמכי
התיק גורסים ...-19. `_is_clean_docket` בדק **צורה בלבד**, ושתי הצורות
תקינות — כך שספרה שגויה אחת עברה את השער וניתקה את השורה מכל הפניה
לתיק האמיתי. `case_number` הוא שדה-זהות; זה לא שדה-תצוגה שאפשר לתקן
בקריאה.

שורש נוסף שהתגלה תוך כדי: ב-1094-09-19 המספר **אינו מופיע ב-full_text
כלל** (הטקסט מתחיל ב"בפני:"), כלומר המודל הפיק ספרות שאין להן עיגון
במקור — בדיוק מה ש-INV-AH בא למנוע.

מה שונה
- `_docket_grounded()` — כל קבוצת-ספרות של ה-docket חייבת להופיע, בסדר,
  בטקסט ההחלטה או בערך שהיא מחליפה. סובלני למפריד (`-` מול `/`), לרווחים
  סביבו, ולתווי-כיווניות (RLM/LRM) שנדחסים בתוך המספר ב-PDF עברי.
  המקורות לעולם אינם פלט-המודל עצמו — אחרת הבדיקה מעגלית.
- סירוב לכתוב מלווה `logger.warning` מפורש (§6) במקום להסתמך על הצורה.
- הפרומפט תוקן: הדוגמאות היו דו-חלקיות בלבד ('1062/24'), מה שהטה נגד
  docket תלת-חלקי של ועדות ערר. נוספו דוגמאות תלת-חלקיות והוראה מפורשת
  לא להשלים/לנחש/לתקן ספרה, ולהחזיר ריק כשהמספר אינו בטקסט.

אימות מול הקורפוס החי (386 שורות): מתוך 50 השורות שהנרמול חל עליהן
בפועל — **0 נחסמות**. הגארד חוסם רק ספרות שאין להן עיגון באף מקור.

invariants: INV-AH (quote-or-retract על שדה-זהות) · G1 (נרמול במקור) ·
§6 (סירוב מדווח, לא נבלע)

טסטים: 7 חדשים (tests/test_docket_grounding.py), הראשון שבהם משחזר בדיוק
את הרגרסיה — שתי הצורות עוברות את בדיקת-הצורה, רק הנכונה מעוגנת.
521 עוברים.
2026-08-05 10:45:22 +00:00
2f50a8cc79 fix(retrieval): 104 החלטות ועדות-ערר היו בלתי-נראות בחיפוש — סינון ולא דירוג (#232)
All checks were successful
Lint — undefined names / undefined-names (pull_request) Successful in 11s
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 40s
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
`search_precedent_library` — הכלי שסוכני הכתיבה קוראים לו בפועל — לא חשף
`source_kind` כלל, וכל שכבה מתחתיו נפלה לברירת-מחדל `external_upload`.
התוצאה: `WHERE cl.source_kind = 'external_upload'` חתך 104 החלטות ועדות-ערר
(29% מהקורפוס) עוד לפני הדירוג. ערר (חיפה) 83/16 — האסמכתה הישירה ביותר
בקורפוס לטענת-סף ס-2 ב-1069-04-26 — לא הוחזרה גם בשאילתה כמעט-מילולית.

ההיפותזה שנרשמה ב-#232 (הטיית verified/cite_count מדירה החלטות חדשות)
**נבדקה ונפסלה**: כיסוי ה-verified כמעט זהה בשני הקורפוסים (55% מול 49%),
ותקרת-ההטיה 0.22 לא הסבירה פער מול התאמה כמעט-מילולית. הסיבה הייתה סינום
קשיח, לא דירוג.

מה שונה
- `_source_kind_clause()` — הגדרה אחת לאוצר-המילים של הסלקטור (G2), במקום
  שכפולו בכל אתר חיפוש/רשימה. `""`/`"all"` = כל הקורפוס; סלקטור לא-מוכר
  מרים ValueError במקום להגיע ל-SQL (הערך מוזרק ב-f-string, אז ה-whitelist
  הוא גם מה ששומר על זה בטוח).
- ברירת-המחדל בכל שרשרת-החיפוש והרשימה: `""` = הקורפוס כולו.
- `search_precedent_library` + `precedent_library_list` חושפים `source_kind`
  לסינון מפורש; `/api/precedent-library/search` מקבל אותו גם הוא כדי ש-UI
  ו-MCP לא יתפצלו.
- נרמול מפריד במספר-תיק: `83/16` ו-`83-16` מחזירים את אותה שורה.

אימות מול ה-DB החי — 4/4 החלטות ועדה חוזרות במקום **1** בשאילתה בלשון
הלכה מאושרת (83-16, 1029-18, 1085-23, 1094-09-19 פדילה). 83/16 עלתה
ל-0.711 מול 0.663 של 3213/97 שחסמה אותה קודם. רשימת-הקורפוס: 259 → 386.

invariants: G1 (נרמול במקור, לא תיקון-תסמין בקריאה) · G2 (הגדרה אחת
לסלקטור; UI ו-MCP על אותו מסלול) · §6 (סלקטור שגוי מתפוצץ, לא נבלע)

טסטים: 5 חדשים (tests/test_source_kind_selector.py), אחד מהם נועל את
ברירות-המחדל של 8 נקודות-הכניסה — זה בדיוק הבאג. 514 עוברים.
2026-08-05 10:32:27 +00:00
720057bc72 fix(aggregator): צד שלם נמחק בשקט — chunking לפי גודל + כשל שמדווח (#233)
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 5s
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
בתיק 1069-04-26 נשלחו 310 טענות עוררים בקריאת-Claude אחת. הקריאה החזירה
לא-JSON, הקוד רשם warning והחזיר [], והפעולה דיווחה status=completed עם
אפס טיעונים לצד המרכזי בערר. 495 propositions_processed — כלומר "עיבדתי
הכל".

שתי תקלות מובחנות, שתיהן מתוקנות:

1. **הקריאה גדולה מדי.** הפיצול הפר-כתב-טענות שנוסף למשיבים (#224) הסתיר
   את זה במקרה — הוא שמר על קריאות קטנות — אבל עוררים וּועדה מדברים בקול
   אחד ואינם מפוצלים לעולם, כך שערר גדול יוצא בקריאה אחת ענקית. ההערה
   בקוד כבר תיעדה בדיוק את הכשל הזה אצל המשיבים; התיקון פשוט לא הוחל על
   העוררים. עכשיו כל צד מעל MAX_PROPS_PER_CALL=80 נצבר בכמה קריאות
   ומשורשר.

   הפיצול הוא trade-off ולא רווח חינם: כל chunk מקובץ בבידוד, ולכן צד
   שמתפצל עלול לקבל יותר טיעונים (וחופפים במקצת) מאשר במעבר יחיד. לאבד
   ליטיגנט שלם גרוע יותר, והחלופה — פרומפט קטן יותר לכל פרופוזיציה —
   הייתה מנוונת כל תיק כדי לתקן את הגדולים. הסדר נשמר בחיתוך, כי טענות
   מגיעות ממוינות לפי claim_index ושכנות שייכות בד"כ לאותו ראש-טיעון.

2. **הכשל נבלע.** החזרת [] על תשובה לא-רשימה אינה ניתנת להבחנה מ"לצד
   הזה אין טיעונים". עכשיו נזרקת AggregationFailed, הקורא רושם אותה
   ב-errors, והסטטוס יורד ל-completed_with_errors (כלל-הנדסה §6).
   ההודעה נוקבת בשם הצד ובמספר הפרופוזיציות, אחרת מפעיל שרואה
   completed_with_errors לא יודע איזה ליטיגנט נעלם.

מבחני רגרסיה: chunking לא מאבד ולא מסדר-מחדש (310→4 קריאות, רצף נשמר);
תשובה לא-רשימה זורקת ומזכירה את שם הצד. אם המבחן השני יחזור אי-פעם
לטעון == [] — באג הבליעה הוחזר.

invariants: כלל-הנדסה §6 — אין בליעה שקטה. G1 — תיקון במקור (גודל הקריאה)
ולא בקריאה. G2/G12 — לא נגועים; שני השערים ירוקים.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:58:48 +00:00
363485d025 refactor(mcp): MCP_HTTP_TOKEN → MCP_HTTP_SHARED_SECRET — זהה לשם ב-Infisical
All checks were successful
Lint — undefined names / undefined-names (pull_request) Successful in 11s
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 5s
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
הסוד נוצר ב-All Infrastructure / env main / /apps/legal-ai בשם
MCP_HTTP_SHARED_SECRET, מתויג credentials, לפי הדפוס של שני טוקני-הגשר
שכבר שם: COURT_FETCH_SHARED_SECRET ו-LEGAL_CHAT_SHARED_SECRET.

שם זהה בקוד וב-Infisical = אין מיפוי שצריך לזכור.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:15:32 +00:00
eebb193fd3 feat(mcp): שער Bearer לתחבורת ה-HTTP — מסרב לעלות בלי טוקן (#231.2)
All checks were successful
Lint — undefined names / undefined-names (pull_request) Successful in 11s
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 4s
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
ב-stdio ההגנה היא הצינור עצמו: רק תהליך שכבר רץ כמשתמש הזה יכול לדבר עם
השרת. streamable-http מבטל את התכונה הזו לחלוטין — כל מי שמגיע ל-socket
יכול לקרוא לכל אחד מ-108 הכלים, ובמרשם יש case_delete,
precedent_library_delete, document_upload וכל כלי כתיבת-הבלוקים. מאזין
לא-מאומת הוא, הלכה למעשה, endpoint למחיקת תיקים.

מימוש דרך ה-TokenVerifier של ה-SDK ו-BearerAuthBackend שלו — לא middleware
משלנו. מסלול-אימות אחד, של המסגרת (G2).

ההחלטות שקובעות את בטיחות הפיצ'ר:
- **מסרב לעלות בלי טוקן.** MissingTokenError קטלנית. החלופה המפתה — לעלות
  ולרשום warning — מייצרת מאזין שנראה בריא ועונה על כל קריאה הרסנית.
  סירוב-אתחול הוא הכשל הבטוח (§6).
- הבנייה בזמן-import ולא בתוך main(): FastMCP מקבל token_verifier ו-auth
  כארגומנטי-בנאי, ולכן טוקן חסר חייב להיכשל *לפני* שה-listener קיים.
- **stdio לא נוגע.** דרישת טוקן שם הייתה שוברת כל סשן אינטראקטיבי בלי שום
  רווח אבטחתי — הגבול שם הוא הצינור.
- השוואה בזמן-קבוע (hmac.compare_digest); == נאיבי מדליף את הטוקן בייט-בייט
  לתוקף שמודד זמנים.
- verify_token מחזיר None ולא זורק — זה אות ה"דחייה" של הפרוטוקול ומניב 401
  נקי במקום 500 שנקרא ככשל-שרת.
- סף אורך 32 תווים; טוקן קצר נדחה באתחול ולא מתגלה מ-access log.
- הטוקן נקרא מ-env (שיאוכלס מ-Infisical), לא מוטמע, ולא נרשם ללוג.

אומת בהרצה חיה:
  HTTP בלי טוקן → סירוב לעלות, exit 1
  stdio בלי טוקן → עולה כרגיל, auth כבוי
  POST בלי Authorization        → 401
  POST עם טוקן שגוי             → 401
  POST עם הטוקן הנכון           → 200
  claude דרך HTTP+Bearer        → 108 כלים, mcp__legal-ai__case_get
  מופעי הטוקן בלוג              → 0

invariants: G2 — מקיים (מסלול-אימות יחיד, של ה-SDK). G12 — מקיים; המודול
נקי מסמלי-פלטפורמה, leak_guard ירוק. INV-AG3 — לא נגוע, השער ירוק.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 07:45:38 +00:00
81ea43a70a feat(mcp): תחבורת streamable-http לצד stdio — פותח את הדרך להזרקת השרת לסשני ACP (#231.1)
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (pull_request) Successful in 42s
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
סוכני הפלטפורמה מקבלים את שרתי ה-MCP שלהם מהלקוח בפתיחת הסשן, והערוץ
הזה מקבל שרתי HTTP בלבד: כל שרת מוזרק כ-
{type:"http", name, url, headers:[Bearer]}. לשרת stdio אין מסלול לשם
בכלל — ומכאן שכל 108 הכלים נעדרו מהסוכנים מאז שמנוע-ההרצה שלהם השתנה.

שרת אחד, שתי תחבורות — לא מימוש שני (G2). מרשם-הכלים, השירותים ובריכת
ה-DB משותפים מילולית; רק פרוטוקול-החוט משתנה, כך שכלי לא יכול להתקיים
בתחבורה אחת ולא באחרת.

- MCP_TRANSPORT בוחר תחבורה, ברירת-מחדל stdio. ערך לא-חוקי נכשל ברעש
  (SystemExit) ולא נופל בשקט חזרה ל-stdio — אחרת ה-listener נעדר בעוד
  שהכול "נראה" תקין (כלל-הנדסה §6).
- stdio נשאר ברירת-המחדל: כל סשן אינטראקטיבי מגיע דרכו מ-~/.claude.json.

מלכודת שהתגלתה בהרצה: אי-אפשר להסתמך על FASTMCP_HOST/FASTMCP_PORT.
ל-FastMCP.__init__ יש ברירות-מחדל מפורשות (host="127.0.0.1", port=8000)
שמועברות ל-Settings(**settings), ובפידנטיק ארגומנטים מפורשים גוברים על
env — כך ש-FASTMCP_PORT מתעלמים ממנו והשרת נקשר ל-8000 בכל מקרה. על
המכונה הזו 8000 תפוס, כך שזה נכשל ברעש במקרה ולא בזכות תכנון. לכן
MCP_HTTP_HOST/MCP_HTTP_PORT נקראים אצלנו ומועברים לבנאי.

ברירת-מחדל loopback (127.0.0.1:8790): התחבורה עדיין ללא אימות, ובמרשם
יש כלים הרסניים (case_delete, precedent_library_delete). שער ה-Bearer
הוא #231.2 והוא חייב לנחות לפני חשיפה מחוץ למארח.

אומת end-to-end: claude התחבר ל-http://127.0.0.1:8791/mcp וקיבל 108
כלים בשם mcp__legal-ai__case_get — זהה בדיוק ל-stdio. כלומר 66 ההענקות,
הוראות-הסוכנים, web/paperclip_client.py ושער INV-AG3 ממשיכים לעבוד.

invariants: G2 — מקיים (שרת אחד, שתי תחבורות, מרשם משותף). G12 — מקיים;
חיווט צד-הפלטפורמה נשאר מאחורי web/agent_platform_port.py, והמודול הזה
נותר נקי מסמלי-פלטפורמה (leak_guard ירוק). INV-AG3 — לא נגוע, השער ירוק.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 07:40:05 +00:00
fbdbc64366 fix(research): עמדת ועדת הערר בטענות-סף נשמרה אך לא נקראה בחזרה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 37s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
הקורא והכותב של analysis-and-research.md לא הסכימו על מהו שדה:
FIELD_LABEL_RE דרש `**כותרת:**` בתחילת שורה, בעוד הכותב התאים את
התווית בכל מקום בשורה. האנליסט כותב טענות-סף כרשימה
(`- **עמדת ועדת הערר:**`) ואת הסוגיות בתחילת שורה — ולכן שמירה
בטענת-סף הצליחה (200 + "✓ נשמר"), אבל הקורא לא ראה את השדה כלל
והעמדה נעלמה ברענון. גם יתר שדות הטענה (טענה/תשובה/שאלה משפטית)
היו בלתי-נראים באותן טענות.

- הקורא מקבל תווית עם סמן-רשימה אופציונלי, כמו הכותב.
- הכותב עובר להשתמש באותה הגדרת-גבולות של הקורא (_chair_field_span)
  במקום regex משלו — `[^*]*?` הישן גם קטע עמדה שהכילה `**הדגשה**`.
  התווית והסמן נשמרים כפי שהם, וכך גם `---` הסוגר.
- תווית מעוטרת (`עמדת ועדת הערר (הכוונת יו"ר 24.6)`) מזוהה בהתאמת-רישא;
  קודם הכותב הוסיף בלוק כפול במקום לעדכן.
- שדה כפול באותו H3 (`### סוגיות 4–6`): הקורא לקח את האחרון והכותב את
  הראשון. שניהם לוקחים עכשיו את הראשון.
- read-after-write: שמירה שהפרסר לא קורא בחזרה מדווחת כשגיאה במקום
  "נשמר" ירוק, וה-UI שומר בקאש את מה שהשרת קרא — לא את מה ששלח.
- תבנית האנליסט (§5) קיבלה שלד מפורש לטענות-סף, זהה לזה של הסוגיות,
  כדי שקבצים חדשים לא ייווצרו במבנה החורג (נרמול-במקור, G1).

הרצת round-trip על כל הקורפוס: 24 מתוך 141 תת-סעיפים נכשלו לפני
התיקון (1017, 1019, 1027, 1033, 1043, 1069, 8124) — 0 אחריו. העמדות
שכבר נשמרו בקבצים הקיימים חוזרות להיקרא בלי מיגרציה.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 14:04:24 +00:00
2ebaa82f85 Merge remote-tracking branch 'origin/main' into worktree-anti-pattern-directive-position
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
2026-07-28 11:53:23 +00:00
86e66cc5bd feat(eval): פילוח אנטי-דפוסים per-ריצה — "איזה כלל הופר", לא רק כמה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
`block_distance_to_final` החזיר `anti_pattern_total` בלבד. ריצת-כיול שמדווחת
"anti=4" אינה יכולה לומר מה לתקן — באבחון ה-A/B של 2026-07-28 נאלצנו להסיק
את הדפוס האשם מהקשר במקום למדוד אותו.

- `anti_by_pattern` (שם-דפוס → מספר-פגיעות) נוסף לתא-המדידה, מ-
  `count_anti_patterns` הקיים — אין ספירה מקבילה.
- `_mean_by_pattern` ממצע על **כל** הריצות: דפוס שלא נורה בריצה נספר כ-0
  ולא מושמט, אחרת הממוצע היה מוטה כלפי מעלה.
- הדוח מקבל טבלת "פילוח אנטי-דפוסים (איזה כלל הופר)" per block×effort×model.

invariants: INV-G8 (eval-harness) · G2 (מרונדר מ-count_anti_patterns/
ANTI_PATTERNS הקנוניים — מקור אחד).

אימות: self-test ALL PASS · 470 passed · בדיקת-שפיות ישירה —
טקסט עם 1 כותרת + 2 תבליטים + 1 פיצול-מיני מפולח נכון ל-
{markdown_headers:1, bullet_lists:2, inline_numbered_fragments:1}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:17:35 +00:00
42ea1a7c58 fix(writer): כלל-הסגנון בסוף הפרומפט — הוא היה שם, במקום שבו הוא לא תופס
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 35s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
הרשימה הקנונית של lessons.ANTI_PATTERNS כבר הוזרקה לכותב, אבל בתו ~46,781
מתוך 46,950 של style_context — שהוא עצמו מקטע אחד מתוך ~12 בפרומפט. הטיוטות
המשיכו לפלוט בדיוק את מה שהיא אוסרת.

A/B מדוד מול הסופיים החתומים (9 תיקים, 60 ייצורים, 2026-07-28) הראה שאותו
כלל, בסוף הפרומפט, חותך anti_pattern_total ב-72–93%:

  block-vav   opus-4-8 1.75→0.12 · opus-5 2.25→0.62
  block-zayin opus-4-8 4.57→0.43 · opus-5 4.43→0.43

וב-distance: −12%/−29% (4-8), −9%/−27% (5). זה שיפור גדול פי-3 מכל הבדל
שנמדד בין המודלים עצמם.

- `lessons.anti_pattern_directive()` — רינדור שני של אותה רשימה קנונית
  (מקור אחד, שתי תצוגות — לא שני כללים).
- מתווסף **אחרון** בשני מסלולי-הכתיבה: `write_block` (בתהליך) ו-
  `get_block_context` (סוכן legal-writer). אילו הוחל רק באחד, שני הכותבים
  היו נפרדים בסגנון (G2).
- **תיקון בליעה-שקטה (§6):** הרשימה הקנונית רונדרה בתוך לולאת-ה-overrides,
  כך שכשל-DB בקטגוריה מוקדמת (golden_ratios) הפיל את הלולאה והשמיט את
  אינווריאנטי-הסגנון כליל — עם אזהרה גנרית בלבד. עכשיו היא מרונדרת ללא
  תנאי, לפני כל קריאת-DB; הערות-היו"ר מתווספות מעליה.

invariants: G11 (תוכן משפטי — סגנון דפנה) · G2 (מקור-אמת יחיד לכלל, ושני
מסלולי-הכתיבה מיושרים) · §6 (אין בליעה שקטה).

בדיקות: 473 passed (3 חדשות — רינדור מלא, שני המסלולים, שרידות לכשל-DB).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:14:47 +00:00
11acdac337 feat(eval): ממד-מודל ל-harness הכיול (#208) — A/B של מודל-ייצור מול הסופיים
`calibrate_effort.py` כייל `effort` בלבד, על מודל נעוץ (GENERATION_MODEL).
כדי להשוות מודל-ייצור (opus-4-8 מול opus-5) מול הסופיים החתומים של דפנה
נדרש ממד שני — ללא מסלול-מדידה מקביל.

- `block_writer.write_block(model_override=…)` — אותו חוזה כמו
  `effort_override` הקיים (נוצר בדיוק ל-#208). מקבל את המזהה הבסיסי בלבד;
  אסקלציית ההקשר-1M (#216) מוחלת מעליו, כך ש-override לא מאבד בשקט את
  חלון ה-1M.
- `--models` ל-harness (ריק = המודל הנעוץ ⇒ ריצת ברירת-המחדל זהה לקודם).
- הדוח מקבל טבלת השוואת-מודלים (block × effort × model) ומסמן  לפי
  אותו דירוג style-clean (#213): anti_total → ratioΔ → distance.
- כל תא מתעד את `model_used` שה-CLI דיווח בפועל; אי-התאמה מסומנת כאזהרת
  fallback-שקט במקום להיזקף בטעות למודל המבוקש.

invariants: INV-G8 (eval-harness — מדידה, לא הרגשה) · G2 (אין מסלול מקביל:
משתמש ב-style_distance/learning_loop הקיימים ובשדה result הקיים
`model_used`) · §6 (אין בליעה שקטה — כשל-תא מדווח ומדולג).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 06:39:33 +00:00
f15cc896d6 feat(claims): אכלוס claims.party_name במקור + backfill — סגירת השדה-המת (#224)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 32s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
עד כה claims.party_name נשאר ריק והצבירה/הכתיבה גזרו את הפרדת-המשיבים
מ-source_document בזמן-ריצה (שדה-מת). עכשיו הוא מאוכלס במקור ונקרא ע"י הצרכנים.

- claims_extractor.extract_and_store_claims: מחתים party_name=source_document
  ל-SPLIT_PARTIES (respondent/permit_applicant); single-voice נשאר ''. אותו
  כלל SPLIT_PARTIES של הצבירה (G2, import לא-מעגלי).
- SCHEMA_V52: backfill idempotent ל-claims קיימים (רק split + party_name ריק).
- argument_aggregator + block_writer._build_claims_context: קוראים
  claims.party_name, עם fallback ל-source_document ל-claims מדור-קודם. התנהגות
  זהה (party_name==source_document) — אפס-רגרסיה, אך השדה כעת מקור-האמת (G1).

py_compile + טעינת-שרשרת-ייבוא נקיים. סוגר את הפתוח האחרון של #224.
Invariants: G1 (אכלוס במקור, לא גזירה-בקריאה), G2 (כלל-פיצול יחיד).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:10:50 +00:00
0fa2281ada fix(block-yod): התייחסות פרטנית לעמדות משיבים מרובים + זיהוי עמדות-מנוגדות (#224)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
השלמת #224 לבלוק י' (דיון והכרעה): הקשר-הטענות כבר מופרד לפי כתב-תשובה
(PR#401, אותו _build_claims_context), אך ה-prompt של block-yod לא הנחה
להתייחס לכל משיב לגופו. נוסף כלל "צדדים מרובים": התייחס לעמדת כל משיב/כתב-תשובה
בנפרד (משיבות 2-3 מול משיבים 4-6), אל תמזג ל"טענות המשיבים" גורפות, וכשעמדות
מנוגדות — ציין והכרע בנימוק.

שינוי-prompt בלבד; משפיע על ניסוח בלוק י' בהחלטות עם כמה משיבים. py_compile ירוק.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:19:37 +00:00
1da1585ca8 fix(block-zayin): הפרדת-משיבים בבלוק ז' (טענות הצדדים) — השלמת #224 לשכבת-הכתיבה
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
בלוק ז' של טיוטת-הביניים מיזג את כל המשיבים לסעיף אחד גנרי (או השמיטם) —
כי (א) _build_claims_context קיבץ רק לפי party_role, ו-(ב) ה-prompt נעל
"מבנה קבוע 3 חלקים: עוררים / ועדה מקומית / מבקשי-היתר" — בלי "משיבים" בכלל.
לכן בתיק 1043-02-26 המשיבים 2-3 ו-4-6 לא הופיעו כצדדים מובחנים, אף שהנתונים
(129 טענות-משיבים מפוצלות לפי כתב-תשובה) תקינים ב-claims. הפרדת-המשיבים של
#224 יושמה בצבירה+פאנל בלבד (PR#392) — לא בכותב-הבלוקים.

- `_build_claims_context` מקבץ לפי (party_role, brief) כאשר brief=source_document
  ל-SPLIT_PARTIES (respondent/permit_applicant) — **אותו כלל בדיוק כמו הצבירה**
  (import SPLIT_PARTIES מ-argument_aggregator, G2). התוצאה: סעיף נפרד לכל
  כתב-תשובה ("טענות המשיבים — כתב תשובה משיבות 2 3", "— משיבים 4 6"...).
- prompt בלוק ז': מבנה-צדדים דינמי לפי הכותרות בקונטקסט; הוראה מפורשת לא למזג
  משיבים שונים ולשקף עמדות-מנוגדות; יעד-אורך גמיש לכמה כתבי-תשובה.

משפיע גם על בלוק י' (אותו _build_claims_context) — הקשר מופרד. py_compile ירוק.
עבודה על MCP → דורש host reload + הרצה-מחדש כדי לראות בפועל.
Invariants: G2 (נגזרת-הפרדה יחידה משותפת עם הצבירה), G1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:46:32 +00:00
9e7ef4fced refactor(export): שם קובץ טיוטת-הביניים → טיוטה-טענות_הצדדים_{N} (הנחיית חיים)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 31s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
במקום "טיוטת-ביניים-v{N}.docx" — שם מדבר יותר: "טיוטה-טענות_הצדדים_{N}.docx".
הפורמט הסופי ("טיוטה-v{N}") לא משתנה.

- docx_exporter._draft_naming: (prefix, sep) פר-מצב; interim=("טיוטה-טענות_הצדדים","_"),
  final=("טיוטה","-v"). glob/parse משתמשים ב-rsplit(sep,1) — נכון גם כשה-prefix
  עצמו מכיל "_".
- app.py _generation_output_ready + frontend pickLatestVersioned: matcher-prefix
  מעודכן; regex-הגרסה עוגן-לסוף /[_v](\d+)$/ (ה-"_" שבתוך הprefix לא מבלבל).
- docstrings + legal-ceo.md + הערות מעודכנים לשם החדש. types.ts (auto-gen)
  יתעדכן ב-api:types.

שינוי-נתון (שם-קובץ), לא layout — בגדר חריג-שער-העיצוב. tsc+eslint+py_compile
ירוקים. הערה: ייצור-השם רץ ב-MCP המקומי (host) → דורש reload; ה-matchers
בקונטיינר (Coolify).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:31:30 +00:00
ab5d34a9d3 feat(hearing): פאנל "מה קרה בדיון" — חשיפת ניתוח-הפרוטוקול ב-UI (#226)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 59s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
תוצר analyze_protocol (איך הטענות הכתובות התפתחו בדיון בעל-פה) היה שכבת
ידע-תיק ללא endpoint ותצוגה — נצרך רק downstream ע"י הכותב בבלוק-י. פאנל חדש
בתחתית טאב "טיעונים ועמדות", מתחת לכרטיס-הטיעונים (INV-IA1: לא טאב-מקביל).

Backend:
- SCHEMA_V51: cases.hearing_attendees JSONB — בית קנוני לנוכחות-בדיון
  (panel_members/appellants_present/respondents_present). נבדל סמנטית
  מ-decisions.panel_members (מותב חותם-ההחלטה) → אין מסלול-מקביל (G2).
- protocol_analyzer._extract_header: מאכלס hearing_attendees; refresh על re-run
  (מתקן #223), לא דורס עם חילוץ ריק. אישור-משתמש: "אפשרות א" (persist+הצגה).
- GET /api/cases/{n}/protocol-analysis — header (תאריך+נוכחים קנוניים) + verdicts
  מקובצים לפי צד. קריאה-בלבד, container-safe, ריק≠שגיאה.
- POST /api/cases/{n}/analyze-protocol — מעיר את המנתח (analyze_protocol מריץ
  claude מקומי, נעדר בקונטיינר) כתבנית aggregate-arguments.
- מגע-Paperclip רק דרך agent_platform_port (G12): wake_analyst_for_protocol_analysis.

Frontend:
- lib/api/protocol-analysis.ts (hooks read+trigger) + HearingChangesPanel
  (סרגל-כותרת, פסי-סינון התחזק/נטען-לראשונה, כרטיסים לפי צד, ציטוט-ראיה).
- חיווט לטאב arguments מתחת ל-LegalArgumentsPanel.

עיצוב: מוקאפ 27-case-hearing-changes-panel אושר ע"י חיים דרך שער Claude Design (X17).
Invariants: מקיים G2 (בית קנוני יחיד, לא מסלול-מקביל), G1 (נרמול-במקור),
G12 (שער-הפלטפורמה), INV-IA1 (לא טאב-מקביל), INV-AH (evidence_quote כבר נאכף במקור).
אימות: py_compile + tsc --noEmit + eslint ירוקים; runtime של ה-endpoint = post-deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:16:35 +00:00
f4ce8332fe fix(protocol): בחירת פרוטוקול ועדת-ערר לפי protocol_scope + document_id (#223)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
analyze_protocol בחר את מסמך-הפרוטוקול הראשון (doc_type='protocol') והתעלם
מ-protocol_scope. בתיק עם כמה פרוטוקולים — נספח ועדה-מקומית + דיון ועדת-הערר —
הוא ניתח את הלא-נכון (התגלה ב-1043-02-26: ניתח "נספח 18 השתלשלות פרוטוקולים"
במקום "פרוטוקול דיון 23.6.26").

- _find_protocol מעדיף פרוטוקול ועדת-ערר (scope שאינו 'lower'); פרוטוקול
  'lower' (ועדה מקומית/מחוזית) לא נכנס להשוואה — הוא רקע לבלוק ו בלבד.
- כמה פרוטוקולי-ערר → האחרון (created_at) גובר; document_id מכוון במפורש.
- analyze_protocol (service + drafting tool + MCP tool) מקבל document_id/
  target_document_id אופציונלי.
- _protocol_scope: קורא metadata.protocol_scope (ריק=appeal, כמו ה-UI).
- 5 בדיקות חדשות: דילוג-lower, all-lower→None, most-recent, targeting, unknown-id.

טווח: Fix1 (קוד). ניקוי 1043 (תיוג נספח 18 כ-lower + מחיקת 11 רשומות שגויות
+ הרצה מכוונת ל-23.6.26) דורש טעינה-מחדש של ה-MCP — אחרי מיזוג. פרוצדורת
ניתוח-מסמך-נוסף הכללית — המשך ב-#223.

Invariants: G1 (נרמול-במקור — קריאת scope מ-metadata, לא ניחוש-בקריאה) ·
G2 (אותו נתיב ניתוח, בורר מדויק) · §6 (no_protocol מחזיר סטטוס מפורש, לא בליעה).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:59:30 +00:00
68c2be2a7b feat(arguments): הפרדת-משיבים בצבירה לפי כתב-תשובה (#224)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 11m48s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
הצבירה קיבצה את כל המשיבים תחת מפלגה אחת "respondent", כך שכתבי-תשובה
נפרדים (משיבות 2-3 מול משיבים 4-6) עם עמדות שעלולות להיות מנוגדות נבלעו
לרשימה אחת. התגלה בתיק 1043-02-26.

השינוי מפצל את הצדדים הרב-משתתפים (respondent/permit_applicant) לפי
source_document — כל כתב-תשובה משותף = יחידת-ליטיגציה קוהרנטית נפרדת —
ומאחסן את התווית ב-legal_arguments.party_name (מיגרציה V50). עוררים/ועדה
נשארים קבוצה יחידה. בונוס: פיצול ה-129 טענות ל-קריאות-Claude קטנות מתקן
את הכשל-בשקט המקורי (קריאה של 100+ פרופוזיציות החזירה non-JSON והפילה את
כל הצד).

- db.py: SCHEMA_V50_SQL — legal_arguments.party_name (ADD COLUMN IF NOT EXISTS, אידמפוטנטי)
- argument_aggregator: קיבוץ (party, party_name); _build_prompt מקבל שם-כתב; SELECT מחזיר party_name
- UI: PartySection מתת-קבץ לפי כתב-תשובה; טיפוס LegalArgument.party_name

טווח v1: הקיבוץ לפי source_document; אכלוס party_name ב-extractor וזיהוי
עמדות-מנוגדות בבלוק ז/י — המשך ב-#224.

Invariants: G1 (נרמול-במקור — party_name בסכמה, לא תיקון-בקריאה) · G2 (אין
מסלול-צבירה מקביל, אותו pipeline) · §6 (אין בליעה שקטה — הכשל-בשקט תוקן).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:42:09 +00:00
cc8af903b7 feat(case-ui): חלוקת רשימת-המסמכים בטאב-הסקירה ל-4 קבוצות
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 36s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
חלוקה פנימית (לנוחות בלבד, לא משנה doc_type) של רשימת מסמכי-התיק:
1. עיקריים (appeal/response/appraisal) · 2. נלווים · 3. פרוטוקול ועדת הערר ·
4. לאחר הדיון. הסיווג נגזר בצד-הלקוח מ-doc_type + שני דגלי-metadata.

שני פקדים חדשים בעורך-התיוג:
- מתג "התקבל אחרי הדיון" → metadata.is_post_hearing (כבר נצרך ע"י בלוק-ח);
  גובר על הסיווג-לפי-סוג ומעביר לקבוצה 4.
- בורר "פרוטוקול של: ועדת הערר / ועדה מקומית-מחוזית" → metadata.protocol_scope
  (נראה רק כשהסוג protocol); ברירת-מחדל appeal→קבוצה 3, lower→קבוצה 2.

Backend: PATCH /documents/{id} + MCP document_update מקבלים is_post_hearing
ו-protocol_scope, נשמרים ב-metadata JSONB (אותו מסלול כמו appraiser_side).

עיצוב אושר ב-Claude Design (X17): 18k-case-documents-grouped.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:52:17 +00:00
2b68d6cbeb fix(block-writer): רתימת חלון-1M של Opus 4.8 לבלוק-י (#216)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
הגבול _MAX_PROMPT_CHARS=400_000 היה שומר מלאכותי שתאם את קיר ה-200K-טוקן
של בניית-Opus הסטנדרטית (~400K תווים עברית @~2 תווים/טוקן) — לא גבול-מודל.
Opus 4.8 מציע חלון 1M-טוקן במחיר רגיל; `claude -p` חושף אותו ישירות כ-model-id
`claude-opus-4-8[1m]` (אומת CLI 2.1.196, בלי beta-header). בלוק-י (דיון), שנושא
את התיק המלא כ-source-context (805K/891K תווים), נחסם רק ע"י השומר המיושן.

שינויים:
- block_writer: GENERATION_MODEL_1M + סף אסקלציה _CTX_1M_THRESHOLD_CHARS=350_000.
  פרומפט > הסף → `[1m]`; אחרת הבנייה הסטנדרטית (בלוקים קטנים נשארים זולים/מהירים).
- _MAX_PROMPT_CHARS 400_000 → 1_500_000 (~750K טוקנים; מרווח מתחת ל-1M) כשומר-קשיח
  סופי גם על בניית-ה-1M; הודעת-השגיאה מפנה לצמצום-source-context.
- model_used (provenance) משקף את הבנייה בפועל (200K/[1m]).
- claude_session: אזהרת-גודל מעודכנת (350K + רמז ל-[1m]).
- ספ 04-analysis-writing §1.3: תיעוד האסקלציה כעידון של אותו pin (לא מסלול מקביל, G2).

Invariants: מקיים G2 (אותו מודל-נעוץ, חלון רחב — לא מסלול מקביל), G1, INV-WR*;
לא בולע שגיאות (שומר-קשיח סופי נשאר). xhigh לבלוק-י ללא שינוי.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:38:00 +00:00
44f6915f0c feat(status): single source of truth for the case-status model + canonicalize intermediates
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
Fixes the case where the H1 chip, the pipeline stepper and the manual-changer
dropdown disagreed (e.g. 1043-02-26 on analyst_verified): the status sat between
the canonical 10 and a legacy bucket, so each surface read a different list.

Root fix — ONE authoritative status model + canonicalize the real intermediate
states (chair-approved):

- New mcp-server/.../case_status_model.py — the single registry: ordered
  StatusDef list (key/label/description/phase/selectable/terminal/on_enter),
  the 5 phases, STATUS_ORDER, phase_of/label_of, and a drift assertion that the
  CaseStatus enum matches it. `on_enter` is the seam for a status to *do*
  something on entry, declared next to its definition (no dispatcher wired yet).
- models.CaseStatus enum: analyst_verified + research_complete promoted to
  first-class canonical statuses (the agents set them — they belong in the
  model, not a legacy fallback).
- tools/cases.py: the forward-only STATUS_ORDER guard now derives from the
  registry instead of an inline list.
- GET /api/status-model exposes the model so the frontend mirror can be
  generated against it.
- web-ui case-status.ts mirrors it: the two intermediates moved from the
  legacy map into CASE_STATUSES / PHASES(thinking) / STATUS_LABELS /
  STATUS_DESCRIPTIONS; status-badge icons+tones extended. So the chip (status),
  stepper (its phase) and dropdown (now includes it) all agree.

Invariants: G2 — collapses the scattered status definitions (enum, inline
STATUS_ORDER, two frontend label maps) onto one backend authority + a documented
frontend mirror; no parallel status list remains. Agent-prompt alignment +
backfill follow in a separate change. py_compile + tsc + eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:31:21 +00:00
88b7dd1004 feat(blocks): block-vav effort medium→low (empirical calibration #208)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
Set block-vav (בלוק ו', רקע/background) generation effort from medium to
low in BLOCK_CONFIG. The #208 empirical effort calibration
(scripts/calibrate_effort.py, report
data/eval/effort-calibration-20260630T155054Z.md, n=7 finals) found that
for block-vav, low effort lands closest to chair Dafna Tamir's finalized
text — winning on BOTH the composite distance metric (low 0.4454 vs
medium 0.4749 vs high 0.5381 vs xhigh 0.5413) AND the cleaner style
signal anti_pattern_total (low 1.57 vs medium 2.29 vs high 4.14). Chair
(chaim) approved adopting low for block-vav.

This is the only block with a clean/confident calibration signal —
block-zayin and block-yod-alef signals were ambiguous/tied and are left
unchanged. No other block effort is touched.

Invariants: G8 (effort empirically calibrated, not by-feel);
claude_session-local-only (block_writer runs via the local MCP/claude_local
generation path). Host-side generation parameter — after merge the host
tree needs `git pull` to take effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:40:26 +00:00
88ca96bf19 perf(citation-verification): cache view + parallelize backend retrieval
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
The "אימות פסיקה" tab reloaded its data on every tab revisit and the first
load took 1.5–3s. Two independent causes, two fixes:

Frontend (web-ui/src/lib/api/citation-verification.ts):
- staleTime 10s → 5min and refetchOnMount: false. Radix Tabs unmounts inactive
  TabsContent, so switching back used to trigger a full rebuild once the 10s
  window elapsed. Verify mutations still invalidate the query, so edits pull
  fresh data.

Backend (mcp-server/.../case_citation_verification.py):
- The per-argument loop ran a Voyage embed + vector search + batched authority
  query sequentially (N args × ~300ms waterfall). Extracted to _fetch() and
  fanned out with asyncio.gather — order preserved, n_verified counter and
  per-argument seen-sets unchanged. First load ~5× faster on multi-argument cases.

Pure read/assembly — no new parallel path (G2), no silent error swallowing
(search/radar still log warnings). Invariants: INV-DIG1/INV-AH unaffected
(still read-only, never cites).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:35:52 +00:00
9402c7302f fix(tz): timezone-aware datetimes — unblock TZ=Asia/Jerusalem (#210)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
Backend follow-up to INV-UI9 (X6 §"אודיט-נלווה"): make server-side
datetimes explicit so flipping the container to TZ=Asia/Jerusalem cannot
silently skew stored or displayed timestamps. Storage stays UTC; human
display is Israel time.

- audit.py: naive datetime.utcnow() → datetime.now(timezone.utc) for the
  TIMESTAMPTZ audit_log.created_at. Under TZ=Asia/Jerusalem the naive UTC
  wall-clock would be read as Israel-local and stored ~3h off. (§6: no
  silent skew.)
- calibrate_effort.py: _ts() report stamp now Israel-time via
  ZoneInfo("Asia/Jerusalem") with a filename-safe "-IL" suffix (script runs
  on the UTC host); added a labeled "שעון ישראל" line to the report header
  so the chair-facing timestamp is unambiguous. --self-test still ALL PASS.
- research_md.py: display fields made deterministic Israel-tz —
  update_chair_position timestamp (datetime.now(IL_TZ)) and parse() mtime
  (datetime.fromtimestamp(..., tz=IL_TZ)) — correct regardless of process TZ.

Left intentionally Israel-local-day date.today()/year-cutoff call sites
untouched (drafting/party_claims_summary/block_writer/court_fetch_supreme)
— a Jerusalem committee wants the Israel calendar day; they resolve
correctly once TZ flips.

Invariants: INV-UI9 (store-UTC / display-Israel, server side); §6
(timezone-aware datetimes, no silent ~3h skew).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:24:17 +00:00
a8dd8ee152 feat(export): INV-EX4 single-template source + fix edit-path justification
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
מקור-תבנית-יחיד לכל מפיקי-מסמכי-ההחלטה: כולם מחילים סגנונות דרך
skills/docx/decision_template.docx בלבד; אסור hand-setting של פונט/גודל.

- docs/spec/06-export.md — INV-EX4 חדש (מקור-תבנית-יחיד, →G2)
- docx_reviser: מסלול-העריכה (revise_draft/apply_user_edit) הכניס פסקאות
  עם jc=right קשיח + David/sz ידני → איבד יישור דו-צדדי של התבנית.
  עכשיו מחיל pStyle מן-הטמפלט (Normal/Heading 2/Quote) + jc=both קנוני,
  בלי rFonts/sz ידני (הגופן/גודל מגיעים מהסגנון).
- שומר-CI test_docx_template_single_source — נכשל על בניית rFonts/sz
  במפיקים מחוץ לטמפלט (חריג מסומן # INV-EX4-ok ל-_mark_run_rtl,
  שמחזק את גופן-התבנית David כעקיפת באג-RTL של Word).
- אומת: build_party_claims_summary_docx (סיכום-מנהלים, WS3) + export_decision
  (סופי/טיוטת-ביניים) כבר תבניתיים ותואמי-INV-EX4.
- ארכוב scripts/exec_summary_1043.py (one-off; הוחלף ב-build_party_claims_summary_docx).
- עדכון 2 טסטים שאימתו את ההתנהגות הישנה (rFonts על run מוכנס).

Invariants: מקיים INV-EX4 (חדש), INV-G2 (מקור-יחיד, אין מסלול-סגנון מקביל),
INV-EX1 (DOCX נגזר). נוגע ב-docx_reviser/docx_exporter/analysis_docx_exporter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:07:21 +00:00
dfb2ffe7ce feat(analysis): דגל "לא-נותח" + ניתוח-מחדש מאחד + בדיקת-השפעה (#201)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
WS2b של עיצוב-מחדש זרימת-העבודה. מאפשר ניתוח כתב-הערר לבד ואז איחוד
מסמך-עיקרי שנוסף מאוחר, בלי force-delete גורף, עם diff ליו"ר.

- SCHEMA_V49: documents.claims_extracted_at + claims_extraction_status
  (אירוע-חילוץ per-מסמך, לא נגזר מ-doc_type), + אינדקס חלקי
  idx_documents_claims_pending. idempotent (ADD COLUMN IF NOT EXISTS).
  V48 שמור ל-#357.
- claims_extractor חותם את המסמך אחרי שמירת/אי-מציאת טענות.
- db.primary_docs_not_analyzed (is_primary V47 + claims_extracted_at IS NULL)
  + mark_document_claims_extracted; _row_to_doc חושף claims_analyzed.
- reanalyze_claims (כלי-MCP): snapshot→חילוץ-מאחד רק למסמכים חדשים/לא-נותחו
  (store_claims מחליף per-source ⇒ טענות אחרות נשמרות)→aggregate force=True
  (מסלול קנוני, מוחק רק legal_arguments)→_impact_diff before↔after ליו"ר.
- workflow_status חושף primary_docs_not_analyzed + next-step.
- ספ: 02-data-model §2ג (דגל לא-נותח), 04-analysis-writing §1.3.
- 5 בדיקות-יחידה ל-_impact_diff/_snapshot (פונקציות טהורות).

Invariants:
- G1 (נרמול-במקור): claims_extracted_at נחתם בנקודת-החילוץ, claims_analyzed
  נגזר ממנו — לא תיקון-בקריאה.
- G2 (מסלול קנוני יחיד / merge-not-fork): reanalyze מרחיב את מסלול
  claims_extractor+argument_aggregator הקיים, לא forks; האיחוד דרך
  store_claims per-source; aggregate force=True מוחק רק נגזר (legal_arguments).
- INV-DM (מודל-נתונים): דגל-אירוע per-מסמך, אינדקס חלקי, מקור-אמת יחיד.
- G10 (שער-אנושי): בדיקת-ההשפעה מוצגת ליו"ר, לא מוחלת אוטומטית.
- INV-TOOL idempotency: SCHEMA_V49 idempotent; ניתוח-מחדש חוזר על מסמך
  כבר-נותח הוא refresh נקי (store_methods replace per-source).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:19:28 +00:00
99574f0eba Merge pull request 'fix(export): תיקוני-תבנית DOCX — חתימות לסוף, א–ד ללא נתונים, "החלטה"=כותרת (#205)' (#355) from worktree-agent-a4e6a6d273c4c3379 into main
Some checks failed
G12 Leak-Guard / leak-guard (push) Has been cancelled
Lint — undefined names / undefined-names (push) Has been cancelled
Build & Deploy / build-and-deploy (push) Has been cancelled
2026-06-30 12:18:41 +00:00
085de937da Merge pull request 'feat(analysis): סיכום-מנהלים מזוקק של טענות הצדדים — מסמך הכנה לדיון (#202, WS3)' (#358) from worktree-agent-abc25c98bc1092fa3 into main
Some checks failed
G12 Leak-Guard / leak-guard (push) Has been cancelled
Build & Deploy / build-and-deploy (push) Has been cancelled
Lint — undefined names / undefined-names (push) Has been cancelled
2026-06-30 12:18:09 +00:00
9e6a441076 Merge remote-tracking branch 'origin/main' into rebase-358
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
# Conflicts:
#	docs/spec/04-analysis-writing.md
2026-06-30 12:16:41 +00:00
eb0653fb80 Merge pull request 'feat(calibration): כיול-אמפירי model×effort מול הסופיים (#208)' (#360) from worktree-agent-a5a22be0318670871 into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
G12 Leak-Guard / leak-guard (push) Successful in 5s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-30 12:15:46 +00:00
f935f166a9 feat(calibration): כיול-אמפירי model×effort מול הסופיים (#208)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
A/B harness שמכייל את ה-effort הנעוץ per-בלוק (#204) מול הסופיים של דפנה:
מייצר מחדש כל בלוק דרך מסלול-הייצור (write_block(effort_override=…) →
claude_session.query → claude -p, Opus 4.8, מקומי-בלבד) ומודד מול הסקשן
המתאים בסופי דרך style_distance.block_distance_to_final (change_percent,
anti_pattern_total, golden-ratio deviation, composite distance). ממליץ
per-בלוק על ה-effort הקרוב-ביותר לסופי.

- scripts/calibrate_effort.py — ההארנס (מודל eval_retrieval.py): --self-test
  (offline, מוכיח מדידה+המלצה, אפס DB/CLI) · --dry-run · --efforts/--blocks/
  --case/--repeats. דוח data/eval/effort-calibration-<ts>.{json,md} עם
  גודל-מדגם בולט — עדות-כיוון, לא רגרסיה (מעט סופיים-עלויים).
- style_distance.py — block_distance_to_final + split_final_by_section
  (מקור-מדידה יחיד, G2; reuse compute_diff_stats/count_anti_patterns/chunker).
- block_writer.py — write_block(effort_override=) להזרקת effort per-קריאה
  בלי לדרוס את ברירות-המחדל הנעוצות; רושם את ה-effort האפקטיבי.
- scripts/SCRIPTS.md — ערך חדש.

Invariants: G8 (eval-harness — מדידה אמפירית, לא הנחה) · G2 (reuse של
style_distance/learning_loop — אין מסלול-מדד מקביל) · claude_session
local-only (reference_claude_generation_path) · INV-LRN4/5 (השוואה מול
הסופי, מדידת-סגנון; אין מהות-תיק נגררת). אומת: --self-test 14/14 PASS.
הריצה החיה host-only (claude CLI) — לא ניתנת-להרצה ב-worktree/קונטיינר.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:13:43 +00:00
8d704e6e40 Merge remote-tracking branch 'origin/main' into rebase-357
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
# Conflicts:
#	mcp-server/src/legal_mcp/services/db.py
2026-06-30 12:09:49 +00:00
faee621f0b Merge pull request 'feat(drafting): טיוטת-ביניים דטרמיניסטית — Opus 4.8 + effort per-בלוק (#204)' (#356) from worktree-agent-a0d2502e8308bc27c into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-30 12:03:42 +00:00
aa2548ff5d feat(analysis): סיכום-מנהלים מזוקק של טענות הצדדים — מסמך הכנה לדיון (#202, WS3)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
מסמך פרוזה נפרד מטיוטת-הביניים (החלטת-יו"ר): תמצית מוקפדת של טענות+תשובות
להכנת היו"ר לדיון. נגזר מ-legal_arguments/claims הקיימים — אותו מקור-אמת,
ללא חילוץ-מחדש (G2). מתוחם לטענות-התיק בלבד (INV-AH).

- כלי-MCP חדש summarize_party_claims (drafting.py + server.py); ייצור עובר
  claude_session נעוץ Opus 4.8 + effort=high (זיקוק/סינתזה); local-only.
- שירות party_claims_summary.py: בחירת-מקור arguments→claims, פרומפט מעוגן-מקור,
  שמירה ל-data/cases/{n}/documents/research/ (git + S3).
- ייצוא DOCX בסגנון-תבנית דפנה (build_party_claims_summary_docx) + endpoints
  read/download/export-docx ב-web/app.py (מגישים בלבד — אין claude CLI בקונטיינר).
- _next_version הוכלל ל-prefix-scoped (תאימות-לאחור) למניעת התנגשות-גרסאות.
- ספ 04-analysis-writing.md §1.3 + 8 בדיקות אופליין (מקור/נעיצת-מודל/markdown→docx).

Invariants: G2 (מקור-אמת יחיד, בלי מסלול מקביל) · X9 TOOL1/TOOL4 (envelope +
extract↔serve) · INV-AH (עיגון-מקור, abstention על טענה לא-ברורה) ·
claude_session local-only · G9 (provenance — נשמר durably + git/S3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:22:20 +00:00
24e3e2fe80 feat(analysis): analyze_protocol — comparative hearing-protocol analysis (WS4 #203)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
מנתח פרוטוקול-דיון מול כתבי-הטענות: אילו טענות ירדו/חוזקו/עלו-חדשות + חידוד
שאלות משפטיות, ומחלץ נתוני כותרת (א–ד). התוצאה נכנסת לידע-התיק.

- protocol_analyzer.py — ניתוח השוואתי דרך claude_session (Opus 4.8, effort=high,
  מקומי בלבד); שער anti-hallucination (quote-or-retract) ב-_normalize_change;
  חילוץ א–ד + הזנת hearing_date חזרה ל-cases (לא דורס קלט-יו"ר).
- claims_extractor: protocol → claim_type='protocol'; extract_claims קולט protocol.
- block_writer._build_claims_context מחריג claim_type='protocol' מבלוק ז (INV-WR4).
- db: טבלת protocol_analysis (V47, idempotent per-doc) + replace/list helpers.
- tools/drafting + server: analyze_protocol + get_protocol_analysis (extract/get).
- ספ 04-analysis-writing §1.3 + 15 בדיקות.

Invariants: G1 (נרמול-במקור: hearing_date לעמודה הקנונית), G2 (ידע-תיק נגזר
ממקור-האמת protocol+legal_arguments, לא מסלול מקביל), INV-TOOL3 (idempotent
per document_id), INV-TOOL4 (extract/get symmetry), INV-AH (quote-or-retract),
INV-WR4 (בלוק ז = טענות-כתב מקוריות בלבד), claude_session local-only, G12 (נקי
מ-Paperclip — leak-guard עובר).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:21:13 +00:00
aad2de9205 feat(drafting): טיוטת-ביניים דטרמיניסטית — נעיצת Opus 4.8 + effort per-בלוק (#204)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
מנגנון-הייצור (claude_session→claude -p) לא העביר model/effort: BLOCK_CONFIG
הגדיר temp+model per-בלוק שהיו מטא-דאטה-מת (block_writer.py:447 העביר רק
prompt/timeout/tools). הייצור רץ על מודל ברירת-המחדל של ה-CLI עם פרומפטים-חופשיים
→ מקור אי-העקביות בטיוטת-הביניים.

תיקון (עקביות-מבנית, WS5):
- נעיצת model="claude-opus-4-8" (GENERATION_MODEL) לכל בלוקי-ה-AI + effort per-בלוק
  שמועבר בפועל דרך claude_session.query → `claude -p --model --effort`.
  מפת-effort: ה=medium · ו=medium · ז=high · ח=medium · ט=high · יא=high · י=xhigh.
- ניסוח-פתיחה קבוע לבלוק-ה (תמיד "לפנינו ערר…", סוף ל-OR "עניינה של החלטה זו").
- מבנה-פנימי קבוע לבלוק-ה; סדר-בלוקים בייצוא כבר קבוע (_INTERIM_BLOCK_ORDER).
- הסרת שדות temp/model המתים מ-BLOCK_CONFIG; "temp" deprecated (Opus 4.7/4.8 דוחים
  temperature→400). decision_blocks.temperature נשמר=0 לתאימות-עמודה בלבד.
- timeout נגזר מ-effort (לא מפיצול sonnet/opus שכבר לא קיים).
- עדכון endpoint reference /api/settings/mcp/blocks + blocks-tab + McpBlock לחשוף effort.
- ספ: docs/block-schema.md §3 (effort מחליף temperature), docs/spec/06-export.md
  (דטרמיניזם-מבני של טיוטת-הביניים).

G2 (מקור-אמת יחיד) — אין מסלול-ייצור מקביל: write_interim_draft מתזמר את אותו
write_and_store_block→write_block של ההחלטה המלאה.
claude_session-local-only — הייצור נשאר דרך גשר-ה-CLI, לא SDK/קונטיינר.
INV-EX1 — סדר-ייצוא דטרמיניסטי נשמר; G11 — עקרונות-הכתיבה ל-12 הבלוקים נשמרים.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:18:56 +00:00
23e088d25e fix(export): clear template tables in _clear_body — חתימות לסוף, א–ד ללא נתונים (#205)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
תיקוני-תבנית DOCX (WS5 ת1–ת3): הטמפלט decision_template.docx מכיל 3 טבלאות-
דוגמה (כותרת מוסדית / הרכב / חתימות). _clear_body הסיר רק <w:p> והשאיר את
הטבלאות. כתוצאה:

- ת3 (שורש): טבלת-החתימות שרדה במיקום-הטמפלט (למעלה, צמוד לטבלת-הכותרת/בלוק-ד),
  וכל תוכן-ההחלטה נדחף אחריה. החתימות הופיעו בראש המסמך במקום בסוף.
- ת1: טבלאות הכותרת/הרכב הזריקו נתוני בלוק-א–ד שלא חולצו מהפרוטוקול.

התיקון בשכבת-הרינדור (G1 — לתקן במקור, לא לשכפל סימפטום): _clear_body מסיר
כעת את כל גוף-המסמך (פסקאות, טבלאות, ו-bookmarks יתומים) ומשאיר רק sectPr.
ההחלטה משוחזרת כולה מ-decision_blocks (INV-EX1) — החתימות מגיעות מ-block-yod-bet
שמרונדר אחרון בסדר-הבלוקים, ולכן טבלאות-הדוגמה הן פיגום מיושן שצריך להימחק.

ת2 כבר תקין: block-dalet מרנדר רק את הכותרת "החלטה" (Heading 1, מתעלם מתוכן-
ה-DB) — הוסף test מפורש לנעילה.

תוקן גם ב-analysis_docx_exporter._clear_body (אותו באג, סימטריה G2).

Tests: 4 regression חדשים + 27 docx-tests עוברים.

Invariants: INV-EX1 (DOCX נתון-נגזר מ-decision_blocks, ללא דליפת תוכן-טמפלט) ·
G1 (תיקון-במקור) · G2 (סימטריה בין שני ה-exporters) · G11/block-schema
(block-ד = כותרת בלבד, block-יב = חתימות בסוף).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:16:39 +00:00
7a7249af38 feat(documents): מסמך-עיקרי — is_primary נגזר + רשימה קנונית (#200)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
WS2 (#200): מבחין בין מסמך-עיקרי (מסמך-מהות שהיו"ר עוקבת אחריו לניתוח)
למשני. רשימה קנונית מאושרת-יו"ר: appeal/response/objection/protocol/
appraisal/decision; כל doc_type אחר = משני.

- db.PRIMARY_DOC_TYPES = מקור-אמת יחיד (tuple) + is_primary_doc_type().
- SCHEMA_V47: documents.is_primary BOOLEAN GENERATED ALWAYS AS
  (doc_type = ANY(PRIMARY_DOC_TYPES)) STORED — Postgres גוזר, אין מסלול-
  כתיבה מקביל, לא יכול לסטות מ-doc_type (G1/G2/INV-DM7, כמו ה-tsvectors).
  אידמפוטנטי (ADD COLUMN IF NOT EXISTS, ביטוי קבוע) + אינדקס חלקי
  idx_documents_primary לשאילתת "עיקריים שטרם-נותחו" (#201).
- _row_to_doc גוזר גם בקריאה (רשומות טרום-מיגרציה) + מוסיף doc_category
  ∈ {primary, secondary}. נחשף דרך document_list / case_get / API.
- 02-data-model.md §2ג מתעד את השדה הנגזר.

Invariants: G1 (נרמול-במקור — נגזר, לא נכתב), G2/INV-DM7 (מקור-אמת
יחיד, אין עמודה כפולה/מסלול מקביל), INV-DM3 (דפוס GENERATED drift-free),
INV-DM6 (ללא enum-TEXT חדש — סיווג נגזר). §6 (אין בליעה שקטה).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:16:26 +00:00
987da57da4 feat(ui): synthesis/superseded badges + merge provenance in lessons-tab (#162)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
עוצב ואושר דרך שער-העיצוב (Claude Design 12c-training-lessons.html).
- lessons-tab: SOURCE_BADGE['synthesis']="סינתזה" (navy מלא) · REVIEW_BADGE['superseded']="מוזג"
  (אפור-מעומעם, לא נדחה); שורות superseded מעומעמות (opacity).
- provenance: לקח-על מציג "⤷ מוזג מ-N לקחים · דחייה תשחזר אותם".
- backend: list_decision_lessons + _lesson_to_json מחזירים synthesized_from; training.ts
  DecisionLesson — 'synthesis'/'superseded' ל-unions + synthesized_from?: string[].

הבאדג'ים היו מוגנים-fallback קודם (ללא קריסה) — כעת מתויגים נכון. closes #162.

בדיקות: py_compile ✓ · leak-guard ✓ · tsc --noEmit ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:59:31 +00:00
a6fb632221 feat(learning): raise writer lessons cap 15 → 60 (config.WRITER_LESSONS_LIMIT) — closes #157
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
עכשיו שהלקחים מנוכי-כפילות (סינתזה #158) ומאושרי-יו"ר, התקרה 15 חתכה בשקט הנחיות-סגנון
מובחנות ו-vetted (רישוי/סגנון 29>15). מעלים ל-60 (env-tunable WRITER_LESSONS_LIMIT) —
מכסה כל shard נוכחי עם מרווח; הסינתזה שומרת על אי-רדודנטיות בצמיחה. ה-WARN על חיתוך נשאר
רשת-הביטחון (חוקה §6) ומפנה לסינתזה/העלאת-התקרה. block_writer צורך דרך config.

Invariants: INV-LRN1/G10 (עדיין רק approved זורם; superseded מוחרג), G2. closes #157 בפועל.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:50:55 +00:00
6c3c6a16c2 tune(learning): lesson-synthesis cluster threshold 0.82 → 0.68 (#158)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
כיול אמפירי (dry-run על 81 לקחים): הלקחים מגוונים (חציון-דמיון ~0.48, מקס' ~0.72), כך
ש-0.82 מיזג 0 אשכולות. 0.68 תופס 5 קונסולידציות אמיתיות (רישוי+השבחה+מבנה), כולן עברו את
שער-ה-drift 0.80 (0.848–0.916) → נאמנות נשמרת. שער-ה-drift נשאר רשת-הביטחון מתחת לסף.

Invariants: INV-LRN8 (כיול-סף בלבד; המנגנון/השערים ללא שינוי). depends-on #158.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:44:55 +00:00
14ab7b0cae feat(learning): lesson synthesis — merge overlapping lessons into richer super-lessons (#158 / INV-LRN8)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
הפתרון-האמיתי לחיתוך-השקט limit=15 (#157): במקום ערימת לקחים גולמיים חופפים, ממזגים
לקחי-סגנון דומים ל"לקח-על" אחד עשיר ומוכלל — הסט שזורם לכותב קטֵן ומשתבח (Authorial
Style Profiling). מחקה את סינתזת-הקנוני (V41/INV-LRN6) על אותה טבלה (G2, אין מאגר-מקביל).

מנגנון (services/lesson_synthesis.py, מסלול-יחיד):
- אשכול greedy לפי cosine (LESSON_SYNTH_CLUSTER_THRESHOLD) בתוך shard practice_area+category.
- מיזוג ע"י claude_session מעוגן-מקור (INV-AH, סגנון-בלבד INV-LRN5, abstain) + שער-drift
  (cosine מול centroid ≥ LESSON_SYNTH_DRIFT_FLOOR).
- לקח-על נכתב source='synthesis' + synthesized_from; המקורות→review_status='superseded'.
- שער מדורג-הפיך (הכרעת-יו"ר): מקורות approved → לקח-על approved (זורם), veto-יו"ר משחזר
  את המקורות (db.revert_lesson_synthesis, מחובר ל-PATCH lessons).
- idempotency: lookup-cosine מול synthesis קיים לפני INSERT.

נגזרות: SCHEMA_V46 (embedding vector(1024) + synthesized_from + ivfflat); כלי-MCP
lesson_synthesize_pending; scripts/backfill_lesson_synthesis.py (--dry-run/--apply, audit CSV);
config LESSON_SYNTH_*; spec INV-LRN8; SCRIPTS.md.

get_recent_decision_lessons ללא שינוי — superseded יוצא (מסנן approved), synthesis נכנס.
UI badges (synthesis/superseded) נדחים לשער-העיצוב (מוגנים ב-fallback, ללא קריסה).

בדיקות: py_compile ✓ · leak-guard G12 ✓ · smoke-test טהור לאשכול/cosine/centroid ✓.
אימות functional מלא (dry-run מול DB+voyage+claude CLI) — בהוסט אחרי-deploy, כמו V41.

Invariants: G2 (מסלול-יחיד, אותה טבלה), INV-AH (עיגון+drift), INV-LRN1/G10 (שער מדורג-הפיך),
INV-LRN5 (סגנון-בלבד), INV-LRN8 (חדש). depends-on #157/#159.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:24:42 +00:00
9455da7567 Merge pull request 'feat(learning): feed corpus-measured ratios to the writer (P2 #7, structural half)' (#345) from worktree-corpus-ratios-writer into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m25s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-28 22:11:13 +00:00
638dbf9b9f feat(learning): feed corpus-measured section ratios to the writer (P2 #7, structural half)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
Golden ratios were measured from the corpus (style_distance.measure_corpus_ratios)
but never reached the writer — it used only the hardcoded STRUCTURE_GUIDANCE text.
So the structural target was frozen, and drafts kept deviating (e.g. discussion
block 10% vs a 40-47% target) with no corrective signal.

_structure_guidance_with_corpus(outcome, practice_area) now appends the
corpus-measured section ratios for the block's outcome to the structure guidance,
at both writer entry points (write_block + get_block_context) via one shared helper
(G2). Because measure_corpus_ratios reads style_corpus — which grows with every
enrolled final (see exemplar-growth, #343) — the writer's structural target now
reflects Dafna's ACTUAL current distribution and improves as the corpus grows.
Deterministic (no LLM); best-effort.

This is the STRUCTURAL half of the voice-profile-refresh (#7). The remaining half —
LLM-regenerating the abstract voice-fingerprint PROSE from the corpus (chair-gated)
— is a distinct larger initiative, deliberately deferred rather than rushed.

Invariants: G1 (the writer gets the live measured target, not a frozen constant),
G2 (one structure-guidance builder shared by both entry points; reuses the existing
measure_corpus_ratios — no parallel measurement), INV-LRN4 (the corpus the loop
grows now feeds the writer's structural target).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:10:39 +00:00
8b23542dec feat(learning): chair-feedback style corrections auto-flow to the writer (P1 #6)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
The chair-feedback chain was DEAD: 27 feedback rows captured, 0 ever became a
lesson the writer reads (no weekly-analysis job ever existed). The chair's own
corrections — the most authoritative signal of all, and exactly "learn from every
returned draft" — went nowhere.

decision_lessons can't carry them: it's FK-coupled to a style_corpus row (a signed
final), which a case-in-progress doesn't have. So chair STYLE feedback instead
rides the discussion_rules channel that already reaches the writer for all blocks —
the same path /training promote uses.

- db.append_global_rule: the locked read-modify-write append, extracted from web
  `_append_methodology_override` into one shared impl (G2). The web function is now
  a thin wrapper that seeds defaults; chair-feedback calls it directly.
- record_chair_feedback (MCP tool): a STYLE-category correction (style/wrong_tone/
  wrong_structure) with a lesson_extracted flows immediately to discussion_rules.
  The chair IS the gate — no separate approval (INV-LRN1 graduated gate). SUBSTANCE
  feedback (missing_content/factual_error/other) is case-specific → recorded only.

Invariants: INV-LRN1 (chair-authored style = highest authority, flows; substance
not auto-flowed), G2 (single append impl shared by promote + feedback), INV-LRN5
(style channel only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:06:58 +00:00
93a9404663 feat(learning): grow style-exemplars from every final (P1 #5)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
The style-exemplar corpus (channel B — the writer's block-level retrieval of
Dafna's real prose) was FROZEN at the one-time seed backfill: new finals were
enrolled into style_corpus but never broken into exemplars, so the richest style
channel never grew (8137/8126/8174 had 0 exemplars). The writer kept retrieving
only March–April seed paragraphs no matter how many finals were signed.

Extract the per-decision exemplar logic (section→paragraph→Voyage-embed→replace)
into a shared service `legal_mcp.services.style_exemplars.extract_and_store` —
the SINGLE implementation now used by BOTH the one-time backfill and the live
enrollment path (G2; no parallel extractor). `_enroll_final_in_library` calls it
on every final upload (source='internal_committee', the same source the writer's
search_style_exemplars reads). Voyage embeds over REST → container-safe;
best-effort, surfaced in the upload response, never fails the upload.

Effect: every signed final now grows the exemplar corpus, so the writer's
block-level style retrieval improves with each decision — the core "learn from
every decision" fix for channel B. Path A (style_distance_history) will track
whether the larger exemplar pool reduces style-distance over time.

Invariants: G2 (one extraction path shared by backfill + enroll), INV-LRN5
(style/structure prose only — substance routes elsewhere), INV-LRN4 (the
draft↔final loop now feeds the exemplar channel, not just the lesson channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:57:38 +00:00
cfcfe4df48 Merge pull request 'fix(learning): כרטיס-אוצֵר כן-3-ערוצים + לכידת ממצאי-אוצֵר (source='curator')' (#339) from worktree-curator-learning-surface into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 5s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-28 21:28:53 +00:00