diff --git a/.claude/agents/legal-analyst.md b/.claude/agents/legal-analyst.md index a5b20d9..4f6bc1a 100644 --- a/.claude/agents/legal-analyst.md +++ b/.claude/agents/legal-analyst.md @@ -455,12 +455,12 @@ X שאלות עומדות להכרעה: ### 8א. אימות פסיקה סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו'). לכל פסק דין שמוזכר: -1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. +1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. הקורפוס כולל גם הלכות מהחלטות ועדות ערר שהועלו (internal_committee). 2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`) 3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות 4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס. 5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס". -6. **אם לא נמצא בכלל** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש. +6. **אם לא נמצא בכלל** — קודם **נסה שוב עם הקשר** (לא שם לבדו): צרף מונחי תוכן או מספר תיק לשאילתה. שם תיק לבדו (`"אגסי"`) אינו מפתח אמין — הוא עלול להחזיר את מי שמצטט את התיק ולא את התיק עצמו. רק אם גם זה ריק — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש. הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2. diff --git a/.claude/agents/legal-researcher.md b/.claude/agents/legal-researcher.md index bcd0b3c..e7dfe2e 100644 --- a/.claude/agents/legal-researcher.md +++ b/.claude/agents/legal-researcher.md @@ -269,9 +269,18 @@ search_internal_decisions( **מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו. +#### 2ב.4א — איתור החלטה ספציפית לפי שם — פרוטוקול לפני "לא בקורפוס" ⚠️ + +שם תיק לבדו (למשל `"אגסי"`) **אינו מפתח חיפוש אמין**. ההטמעה הסמנטית והאינדקס הלקסיקלי בנויים על תוכן ההלכה/הפסקה — כך ששאילתת-שם עלולה להחזיר דווקא החלטות ש**מצטטות** את התיק, ולא את התיק עצמו. לפני שמכריזים שהחלטה אינה בקורפוס: + +1. **הוסף הקשר לשאילתה** — לא `"אגסי"` אלא `"אגסי פטור 19(ג)(1) שתי דירות 140 מ"ר"`, או חפש לפי **מספר התיק** (`"ערר 81002-01-21"`). +2. **חפש בשני הקורפוסים** — `search_precedent_library` **וגם** `search_internal_decisions`. החלטות ערר/בל"מ שהיו"ר מעלה נשמרות כ-`internal_committee` ומתגלות בחיפוש הפנימי. +3. **לאימות קיום / דפדוף** — `precedent_library_list(search="<שם>", source_kind="all_committees")`. ברירת המחדל `external_upload` **מסתירה** החלטות ועדת ערר שהועלו — חובה `all_committees` או `internal_committee`. +4. רק אם **כל** הניסיונות לעיל ריקים — הכרז "לא בקורפוס" ועבור ל-2ב.5. + #### 2ב.5 — תיעוד פסיקה חסרה (`missing_precedent_create`) — חובה -**מתי לקרוא:** לכל ציטוט שהצדדים הביאו (בכתב ערר / תגובה / תגובת ועדה) **שלא נמצא בקורפוס** אחרי חיפוש מובנה (`search_precedent_library` + `search_internal_decisions` + `precedent_search_library`). +**מתי לקרוא:** לכל ציטוט שהצדדים הביאו (בכתב ערר / תגובה / תגובת ועדה) **שלא נמצא בקורפוס** אחרי חיפוש מובנה לפי פרוטוקול 2ב.4א (`search_precedent_library` + `search_internal_decisions` + `precedent_search_library`, כולל שאילתה עם הקשר/מספר תיק). **למה זה חשוב:** - ה-writer יודע שלא להסתמך על פסיקה שלא ב-DB ("טוענים שמופיע" ≠ "אומת") diff --git a/.claude/agents/legal-writer.md b/.claude/agents/legal-writer.md index d212b52..60c8598 100644 --- a/.claude/agents/legal-writer.md +++ b/.claude/agents/legal-writer.md @@ -351,6 +351,8 @@ fi חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד. +**איתור החלטה לפי שם:** אם אתה מחפש החלטה ספציפית בשמה (למשל "אגסי"), אל תחפש בשם לבדו — צרף מונחי תוכן או מספר תיק (`"אגסי 19(ג)(1) 140 מ"ר"` / `"ערר 81002-01-21"`). שאילתת-שם בלבד עלולה להחזיר את מי שמצטט את ההחלטה ולא את ההחלטה עצמה. + ### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind` כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה: diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 293b8a2..ee8eaf3 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -1388,50 +1388,59 @@ { "id": 1, "title": "Audit + migration practice_area (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197)", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 2, "title": "Audit + reclassify case_law source_kind external_upload → internal_committee עבור 'ערר' prefix", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 3, "title": "Delete + re-extract halachot עבור רשומות שעברו reclassification", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 4, "title": "תיקון נתיב יצירת תיק לתיוג practice_area נכון מההתחלה", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 5, "title": "תיקון /api/precedent-library/upload לניתוב לפי תחילית הציטוט", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 6, "title": "מבחני רגרסיה לכל 3 הbaגים", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 7, "title": "תיקון MCP `case_update` + API `PUT /api/cases/{case_number}` לתמוך בעדכון practice_area + appeal_subtype", "status": "done", - "details": "התגלה ב-26/05/2026: MCP tool case_update והAPI לא מקבלים את השדה practice_area, ולכן אי-אפשר לתקן תיוג שגוי דרך הממשק. נאלצתי לעדכן ידנית ב-SQL. צריך להוסיף את השדות ל-CaseUpdateRequest ב-web/app.py וב-cases_tools.case_update בmcp-server." + "details": "התגלה ב-26/05/2026: MCP tool case_update והAPI לא מקבלים את השדה practice_area, ולכן אי-אפשר לתקן תיוג שגוי דרך הממשק. נאלצתי לעדכן ידנית ב-SQL. צריך להוסיף את השדות ל-CaseUpdateRequest ב-web/app.py וב-cases_tools.case_update בmcp-server.", + "parentId": "undefined" }, { "id": 8, "title": "[prevention] DB CHECK constraints: source_kind='internal_committee' ⇒ chair_name NOT NULL; cases.practice_area enum", "status": "done", - "description": "מיגרציה: ALTER TABLE case_law ADD CONSTRAINT chair_required_for_internal CHECK (source_kind <> 'internal_committee' OR (chair_name IS NOT NULL AND chair_name <> '')); וכן CHECK על cases.practice_area לערכים תקינים. חייב לרוץ אחרי subtask #2 (backfill) אחרת constraint creation ייכשל." + "description": "מיגרציה: ALTER TABLE case_law ADD CONSTRAINT chair_required_for_internal CHECK (source_kind <> 'internal_committee' OR (chair_name IS NOT NULL AND chair_name <> '')); וכן CHECK על cases.practice_area לערכים תקינים. חייב לרוץ אחרי subtask #2 (backfill) אחרת constraint creation ייכשל.", + "parentId": "undefined" }, { "id": 9, "title": "[prevention] Unify practice_area taxonomy — מיפוי או מחיקה של appeals_committee מ-practice_area.py", "status": "done", - "description": "ב-mcp-server/src/legal_mcp/services/practice_area.py:21 יש PRACTICE_AREAS={appeals_committee,national_insurance,labor_law} שסותר את ה-DB constraint של case_law (rishuy_uvniya/betterment_levy/compensation_197). grep מקיף לכל caller של 'appeals_committee'; להחליף במיפוי מפורש או למחוק." + "description": "ב-mcp-server/src/legal_mcp/services/practice_area.py:21 יש PRACTICE_AREAS={appeals_committee,national_insurance,labor_law} שסותר את ה-DB constraint של case_law (rishuy_uvniya/betterment_levy/compensation_197). grep מקיף לכל caller של 'appeals_committee'; להחליף במיפוי מפורש או למחוק.", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T08:35:22.762800Z" @@ -1452,19 +1461,22 @@ "id": 1, "title": "Backfill chair_name + district לכל 7 הרשומות החסרות (LLM extraction)", "status": "done", - "description": "psql query: SELECT id, case_number FROM case_law WHERE source_kind='internal_committee' AND (chair_name IS NULL OR chair_name=''); לכל אחת — חילוץ ע\"י precedent_metadata_extractor.extract_and_apply." + "description": "psql query: SELECT id, case_number FROM case_law WHERE source_kind='internal_committee' AND (chair_name IS NULL OR chair_name=''); לכל אחת — חילוץ ע\"י precedent_metadata_extractor.extract_and_apply.", + "parentId": "undefined" }, { "id": 2, "title": "[prevention] Validation: chair_name+district required ב-internal_decisions_upload (API+MCP)", "status": "done", - "description": "ב-web/app.py:4607-4680 כיום chair_name/district = Form(\"\") (default ריק). שנה ל-required עם validation שדוחה ריק כשsource_kind='internal_committee'. הוסף enum של 6 ערכי district (ירושלים/מרכז/תל אביב/צפון/דרום/ארצי)." + "description": "ב-web/app.py:4607-4680 כיום chair_name/district = Form(\"\") (default ריק). שנה ל-required עם validation שדוחה ריק כשsource_kind='internal_committee'. הוסף enum של 6 ערכי district (ירושלים/מרכז/תל אביב/צפון/דרום/ארצי).", + "parentId": "undefined" }, { "id": 3, "title": "[prevention] UI dropdown ל-district בטופס העלאת החלטות ועדה (web-ui)", "status": "done", - "description": "במקום free-text — Select של 6 הערכים. גם בטופס חיפוש (search_internal_decisions)." + "description": "במקום free-text — Select של 6 הערכים. גם בטופס חיפוש (search_internal_decisions).", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T08:35:22.762800Z" @@ -1521,32 +1533,38 @@ { "id": 1, "title": "Migration + model missing_precedents", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 2, "title": "API endpoints POST/GET/upload/PATCH /api/missing-precedents", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 3, "title": "MCP tools missing_precedent_create/list/close", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 4, "title": "Next.js page /missing-precedents עם list + detail + upload form", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 5, "title": "Auto-creation hook במחקר (legal-researcher יוצר רשומה כשמזהה ציטוט חסר)", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 6, "title": "Webhook עדכון לפלאגין Paperclip + Comment לחיים", - "status": "done" + "status": "done", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T08:35:22.762800Z" @@ -1564,27 +1582,32 @@ { "id": 1, "title": "Migration + models legal_arguments + legal_argument_propositions", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 2, "title": "LLM aggregation job (Hermes/DeepSeek profile)", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 3, "title": "API + MCP tool aggregate_claims_to_arguments", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 4, "title": "UI display update — case detail page מציג טיעונים אמיתיים", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 5, "title": "Backfill לכל התיקים הקיימים", - "status": "done" + "status": "done", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T08:35:22.762800Z" @@ -1602,27 +1625,32 @@ { "id": 1, "title": "הוספת 3 ערכי enum ל-practice_area.py APPEALS_COMMITTEE_SUBTYPES", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 2, "title": "כתיבת 3 templates מתודולוגיים ב-docs/methodology/extension-request-{type}.md", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 3, "title": "אוטו-זיהוי בקוד יצירת תיק (subject='בקשה להארכת מועד' → קביעת subtype לפי practice_area)", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 4, "title": "UI badge + filter ייעודי לבל\"מ", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 5, "title": "עדכון web/paperclip_client.py mapping ל-company עבור 3 הערכים החדשים", - "status": "done" + "status": "done", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T08:35:22.762800Z" @@ -1647,55 +1675,65 @@ { "id": 1, "title": "עדכון .claude/agents/legal-ceo.md — routing + statuses + wake reasons", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 2, "title": "עדכון .claude/agents/legal-analyst.md — practice_area, legal_arguments, בל\"מ detection", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 3, "title": "עדכון .claude/agents/legal-researcher.md — 2 layers, missing_precedents, citations, בל\"מ templates", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 4, "title": "עדכון .claude/agents/legal-writer.md — legal_arguments view, בל\"מ templates", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 5, "title": "עדכון .claude/agents/legal-qa.md — בל\"מ-aware validation", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 6, "title": "עדכון .claude/agents/HEARTBEAT.md — כללי routing משותפים + research_complete status", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 7, "title": "סנכרון לכל החברות CMPA mirror — sync_agents_across_companies.py", - "status": "done" + "status": "done", + "parentId": "undefined" }, { "id": 8, "title": "[alignment] researcher docs: דרישה מפורשת שכל 'ערר' → internal_decision_upload, לעולם לא precedent_library_upload", "status": "done", - "description": "בלגל-researcher.md: דוגמת קוד מפורשת + flowchart החלטה: לפי תחילית הציטוט. הסבר על השלילה של precedent_library_upload כשמדובר ב-ערר. תלוי במשימה #39 (MCP tool חדש)." + "description": "בלגל-researcher.md: דוגמת קוד מפורשת + flowchart החלטה: לפי תחילית הציטוט. הסבר על השלילה של precedent_library_upload כשמדובר ב-ערר. תלוי במשימה #39 (MCP tool חדש).", + "parentId": "undefined" }, { "id": 9, "title": "[alignment] analyst docs: הסבר על 2 taxonomies של practice_area + מתי משתמשים בכל אחת", "status": "done", - "description": "בלגל-analyst.md: טבלה ברורה — practice_area (case_law) vs practice_area (cases). מתי להעביר rishuy_uvniya ומתי appeals_committee. אחרי משימה #30.9 (taxonomy unification) — סביר שזה ייפשט." + "description": "בלגל-analyst.md: טבלה ברורה — practice_area (case_law) vs practice_area (cases). מתי להעביר rishuy_uvniya ומתי appeals_committee. אחרי משימה #30.9 (taxonomy unification) — סביר שזה ייפשט.", + "parentId": "undefined" }, { "id": 10, "title": "[alignment] writer docs: הבחנה בין source_kind בציטוט (binding vs persuasive)", "status": "done", - "description": "בלגל-writer.md: 'החלטת ועדת ערר אחרת ⇒ עקביות אופקית, לא הלכה מחייבת'. 'פס\"ד עליון/מנהלי ⇒ סמכותי בינדינג'. דוגמאות פרזיולוגיה מ-skills/decision/SKILL.md." + "description": "בלגל-writer.md: 'החלטת ועדת ערר אחרת ⇒ עקביות אופקית, לא הלכה מחייבת'. 'פס\"ד עליון/מנהלי ⇒ סמכותי בינדינג'. דוגמאות פרזיולוגיה מ-skills/decision/SKILL.md.", + "parentId": "undefined" } ], "updatedAt": "2026-05-26T07:41:47.880478Z" @@ -1870,17 +1908,96 @@ "priority": "low", "subtasks": [], "updatedAt": "2026-05-26T11:27:09.039154Z" + }, + { + "id": "52", + "title": "[Retrieval RC-A] הוספת case_name + case_number ל-tsvector הלקסיקלי", + "description": "השורש האמיתי לכך שסוכן לא מאתר החלטה לפי שם (אגסי). ה-tsvector הלקסיקלי (SCHEMA_V12_SQL ב-db.py) בנוי רק מ-precedent_chunks.content ומ-halachot rule/quote/reasoning — לא משם התיק/הצד או ממספר התיק. לכן שאילתת-שם מחזירה את מי שמצטט את ההחלטה, לא את ההחלטה עצמה. לשלב את case_law.case_name + case_number באינדקס הלקסיקלי (tsvector ייעודי על case_law או setweight) כך שחיפוש לפי שם יפגע ברשומה עצמה.", + "status": "done", + "priority": "high", + "dependencies": [], + "details": "קבצים: mcp-server/src/legal_mcp/services/db.py (SCHEMA_V12_SQL ~שורה 774, search_precedent_library_lexical), hybrid_search.py (_merge_sem_lex). דורש ALTER TABLE + migration על Postgres (localhost:5433) + restart MCP server. בדיקה: search_internal_decisions('אגסי') ו-search_precedent_library('אגסי') חייבים להחזיר את אגסי (1a87efe5) בעמוד הראשון.", + "testStrategy": "reproduction test: query='אגסי' → expect case_law_id 1a87efe5 in top-3. regression: substantive query עדיין מחזיר 0.6+ score.", + "subtasks": [], + "updatedAt": "2026-05-30T11:05:36.307Z" + }, + { + "id": "53", + "title": "[Retrieval RC-B] חיפוש/רשימה מאוחדים — לא לחתוך internal_committee", + "description": "החלטות ערר/בל\"מ שמועלות נשמרות source_kind='internal_committee'. precedent_library_list ברירת מחדל external_upload ומסתיר אותן; כלי ה-MCP precedent_library_list אפילו לא חושף פרמטר source_kind, כך שסוכן לעולם לא יכול לדפדף בהן. לחשוף source_kind/all_committees בכלי ה-MCP ובמידת הצורך לאחד את שכבת ה-list/search.", + "status": "done", + "priority": "high", + "dependencies": [ + "52" + ], + "details": "קבצים: web/app.py (precedent_library_list ~5194, all_committees expansion ב-db.list_external_case_law ~2708), mcp-server tool def ל-precedent_library_list. בדיקה: precedent_library_list יכול להחזיר את אגסי כשמבקשים committees; חיפוש סמנטי כבר מאוחד (אומת).", + "testStrategy": "precedent_library_list(source_kind='all_committees', practice_area='betterment_levy') כולל את אגסי+וינפלד. regression: ברירת מחדל external_upload עדיין מחזירה 14 ולא שוברת UI.", + "subtasks": [], + "updatedAt": "2026-05-30T11:09:44.511Z" + }, + { + "id": "54", + "title": "[Retrieval RC-3] הנחיית סוכנים — איתור לפי שם + שני קורפוסים", + "description": "לעדכן הנחיות legal-analyst/researcher/writer: לאיתור החלטה ספציפית לפי שם להוסיף מונחי תוכן או מספר תיק, ולחפש בשני הקורפוסים (search_internal_decisions + search_precedent_library) לפני שמסיקים 'לא קיים בקורפוס'. כולל יצירת missing_precedent רק אחרי חיפוש כפול.", + "status": "done", + "priority": "medium", + "dependencies": [ + "53" + ], + "details": "קבצים: .claude/agents/legal-analyst.md, legal-researcher.md, legal-writer.md. אחרי שינוי skills/agent config — להריץ sync_agents_across_companies.py.", + "testStrategy": "קריאת ההנחיות מאשרת fallback ברור; (אם אפשר) הרצת סוכן על שאילתת-שם מחזירה את ההחלטה.", + "subtasks": [], + "updatedAt": "2026-05-30T11:12:44.727Z" + }, + { + "id": "55", + "title": "[Retrieval RC-4] תיקון chunking — פרגמנטים זעירים", + "description": "בתוצאות החיפוש מופיעים chunks של מילה-שתיים ('דיון','דיון וב','סיכום ו') כתוצאות מובילות. מציפים תוצאות ומורידים דירוג תוכן אמיתי. לחקור את chunker.py (פיצול לפי כותרת-סעיף שיוצר chunks ריקים) ולתקן: מינימום אורך chunk / מיזוג כותרת לגוף.", + "status": "done", + "priority": "medium", + "dependencies": [ + "54" + ], + "details": "קבצים: mcp-server/src/legal_mcp/services/chunker.py (SECTION_PATTERNS). דורש שיקול re-chunk לרשומות קיימות — לבדוק עלות מול feedback_no_reocr_retrofit (להשתמש בטקסט שמור, לא re-OCR).", + "testStrategy": "אין chunks < N תווים בקורפוס אחרי תיקון; search_internal_decisions('אגסי') לא מציג פרגמנטי 'דיון'.", + "subtasks": [], + "updatedAt": "2026-05-30T11:19:23.923Z" + }, + { + "id": "56", + "title": "[Retrieval finding] halacha_filters לא מסננים source_kind — דליפה חוצת-קורפוסים", + "description": "התגלה תוך כדי משימה 53. ב-search_precedent_library_semantic וב-search_precedent_library_lexical (db.py): chunk_filters כוללים cl.source_kind=$sk אבל halacha_filters כוללים רק review_status. תוצאה: search_precedent_library(external) מחזיר גם הלכות internal_committee, ו-search_internal_decisions(internal) מחזיר גם הלכות external. אי-עקביות: chunks מסוננים, halachot לא. כרגע זה דווקא מסייע למציאוּת (לכן לא רגרסיה), אבל לא עקבי. דורש החלטת מדיניות: או לסנן halachot גם לפי source_kind (עקבי, אך 'מסתיר' שכבות), או להשאיר מאוחד במכוון + לתעד. אם משאירים מאוחד — לעדכן docstrings של שני הכלים שזה לא 'corpus נפרד'.", + "status": "pending", + "priority": "low", + "dependencies": [], + "details": "db.py: search_precedent_library_semantic (~שורה הקודמת ל-3311), search_precedent_library_lexical (3346). שתי הפונקציות: halacha_filters=['h.review_status IN ...'] — חסר cl.source_kind. נמצא בעת בדיקת רגרסיה למשימה 53.", + "testStrategy": "לאחר החלטה: אם מסננים — search_precedent_library('...substantive...', external) לא מחזיר case_law_id internal; אם משאירים — docstring מעודכן + טסט מאשר התנהגות מכוונת.", + "subtasks": [], + "updatedAt": "2026-05-30T11:09:30.257989+00:00" + }, + { + "id": "57", + "title": "[Retrieval #55 follow-up] re-chunk+re-embed של פסיקה שהוטמעה לפני תיקון ה-chunker", + "description": "משימה 55 תיקנה את ה-chunker (עיגון כותרות + מיזוג) ומסננת את 484 הפרגמנטים בזמן query. הרמדיאציה המלאה: re-chunk מ-full_text השמור (ללא re-OCR — תואם feedback_no_reocr_retrofit) + re-embed, כדי שהתוכן יהיה נכון ולא רק מוסתר. נדחה כי זו מיגרציית-נתונים עם עלות Voyage API על ~13+ תיקים — דורש אישור עלות מ-chaim לפני הרצה. לבדוק כמה תיקים מושפעים (יש להם chunk<50) ולהריץ בקבוצות.", + "status": "pending", + "priority": "low", + "dependencies": [ + "55" + ], + "details": "מקור: case_law.full_text קיים. נתיב: chunker.chunk_document(_hierarchical) → embeddings → החלפת precedent_chunks לתיק. למחוק chunks ישנים של התיק לפני הוספה. אחרי הרצה — ניתן להסיר את פילטר ה->=50 query (אופציונלי). תיקים מושפעים: SELECT DISTINCT case_law_id WHERE length(trim(content))<50.", + "testStrategy": "אחרי re-chunk לתיק לדוגמה: 0 chunks<50 לאותו case_law_id; search_internal_decisions עדיין מחזיר את התיק; ספירת chunks סבירה.", + "subtasks": [], + "updatedAt": "2026-05-30T11:19:06.142606+00:00" } ], "metadata": { "version": "1.0.0", - "lastModified": "2026-05-04T17:29:25.687Z", - "taskCount": 29, - "completedCount": 24, + "lastModified": "2026-05-30T11:19:23.923Z", + "taskCount": 57, + "completedCount": 52, "tags": [ "legal-ai" - ], - "updated": "2026-05-26T06:39:31.733370" + ] } } } \ No newline at end of file diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index a14d85b..8aa9c3d 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -201,11 +201,20 @@ async def precedent_library_list( precedent_level: str = "", source_type: str = "", search: str = "", + source_kind: str = "external_upload", limit: int = 100, ) -> str: - """רשימת הפסיקה בקורפוס הסמכותי, עם פילטרים.""" + """רשימת הפסיקה בקורפוס, עם פילטרים. + + source_kind: 'external_upload' (ברירת מחדל — פס"ד בתי משפט) / + 'internal_committee' (החלטות ועדות ערר ערר/בל"מ שהועלו) / + 'all_committees' (שתיהן — internal + appeals_committee). + החלטות ערר/בל"מ שמעלים נשמרות כ-internal_committee — כדי לראותן + ברשימה השתמש ב-source_kind='internal_committee' או 'all_committees'. + """ return await plib.precedent_library_list( - practice_area, court, precedent_level, source_type, search, limit, + practice_area, court, precedent_level, source_type, search, + source_kind, limit, ) diff --git a/mcp-server/src/legal_mcp/services/chunker.py b/mcp-server/src/legal_mcp/services/chunker.py index 99f8938..d67f5cf 100644 --- a/mcp-server/src/legal_mcp/services/chunker.py +++ b/mcp-server/src/legal_mcp/services/chunker.py @@ -97,13 +97,32 @@ def _assign_pages(chunks: list[Chunk], text: str, page_offsets: list[int]) -> No pos = idx + max(1, len(c.content) // 2) +# A section shorter than this (stripped chars) is not a real section — it's +# an artifact of a header keyword matched mid-text. Such a fragment is merged +# into the preceding section rather than emitted as its own chunk. See #55: +# unanchored keywords like "דיון"/"החלטה"/"מסקנה" appearing inside a sentence +# used to carve tiny boundary chunks ("דיון). במסגרת ה") that polluted search. +MIN_SECTION_CHARS = 60 + + def _split_into_sections(text: str) -> list[tuple[str, str]]: - """Split text into (section_type, text) pairs based on Hebrew headers.""" + """Split text into (section_type, text) pairs based on Hebrew headers. + + Header keywords are matched only at the **start of a line** (after + optional whitespace / list numbering like ``5.`` or ``ג.``). A real + section header in these decisions sits on its own line; anchoring to + the line start prevents common words ("דיון", "החלטה", "מסקנה") that + appear mid-sentence from being treated as section boundaries — which + previously produced tiny fragment chunks (#55). + """ # Find all section headers and their positions markers: list[tuple[int, str]] = [] for pattern, section_type in SECTION_PATTERNS: - for match in re.finditer(pattern, text): + # ^ + MULTILINE: line start only. Optional leading spaces/tabs and an + # optional ordinal prefix ("5.", "5)", "ג.") before the keyword. + anchored = rf"^[ \t]*(?:\d+[.)]\s*|[א-ת][.)]\s*)?(?:{pattern})" + for match in re.finditer(anchored, text, re.MULTILINE): markers.append((match.start(), section_type)) if not markers: @@ -120,11 +139,18 @@ def _split_into_sections(text: str) -> list[tuple[str, str]]: if intro_text: sections.append(("intro", intro_text)) - # Each section + # Each section. A section whose text is too short to stand alone is + # merged into the previous section (keeping the previous type) so a + # near-adjacent pair of headers can't produce a fragment chunk. for i, (pos, section_type) in enumerate(markers): end = markers[i + 1][0] if i + 1 < len(markers) else len(text) section_text = text[pos:end].strip() - if section_text: + if not section_text: + continue + if len(section_text) < MIN_SECTION_CHARS and sections: + prev_type, prev_text = sections[-1] + sections[-1] = (prev_type, f"{prev_text}\n{section_text}") + else: sections.append((section_type, section_text)) return sections diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 8416424..4c98f1b 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -1070,6 +1070,29 @@ ALTER TABLE case_law ADD COLUMN IF NOT EXISTS citation_formatted TEXT DEFAULT '' """ +# ── V20: case-name / case-number lexical match ──────────────────── +# RC-A fix: the V12 tsvectors cover only chunk *content* + halacha +# text, so a bare case-name query ("אגסי") matched decisions that +# *cite* the case rather than the case itself. case_name and +# case_number live on the parent case_law row, so we add a dedicated +# meta tsvector there and OR it into the lexical search — a name/number +# hit then surfaces all of that case's chunks + halachot. 'simple' +# config (no stemmer) preserves Hebrew names + alphanumeric case +# numbers like "81002-01-21" exactly as V12 does for content. +SCHEMA_V20_SQL = """ +ALTER TABLE case_law + ADD COLUMN IF NOT EXISTS meta_tsv tsvector + GENERATED ALWAYS AS ( + to_tsvector('simple', + coalesce(case_name,'') || ' ' || coalesce(case_number,'') + ) + ) STORED; + +CREATE INDEX IF NOT EXISTS idx_case_law_meta_tsv + ON case_law USING GIN(meta_tsv); +""" + + async def _run_schema_migrations(pool: asyncpg.Pool) -> None: async with pool.acquire() as conn: await conn.execute(SCHEMA_SQL) @@ -1092,7 +1115,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None: await conn.execute(SCHEMA_V17_SQL) await conn.execute(SCHEMA_V18_SQL) await conn.execute(SCHEMA_V19_SQL) - logger.info("Database schema initialized (v1-v19)") + await conn.execute(SCHEMA_V20_SQL) + logger.info("Database schema initialized (v1-v20)") async def init_schema() -> None: @@ -3217,6 +3241,9 @@ async def search_precedent_library_semantic( ON parent.id = pc.parent_chunk_id WHERE {' AND '.join(chunk_filters)} AND pc.embedding IS NOT NULL + -- #55: exclude tiny fragment chunks (artifacts of pre-fix + -- mid-sentence header splits) that carry no retrievable signal. + AND length(trim(pc.content)) >= 50 ORDER BY pc.embedding <=> $1 LIMIT $2 """ @@ -3411,11 +3438,17 @@ async def search_precedent_library_lexical( h.practice_areas, h.subject_tags, h.confidence, h.rule_type, cl.case_number, cl.case_name, cl.court, cl.date AS decision_date, cl.precedent_level, cl.chair_name, cl.district, - ts_rank_cd(h.rule_tsv, plainto_tsquery('simple', $1)) AS score + GREATEST( + ts_rank_cd(h.rule_tsv, plainto_tsquery('simple', $1)), + ts_rank_cd(cl.meta_tsv, plainto_tsquery('simple', $1)) + ) + + CASE WHEN cl.meta_tsv @@ plainto_tsquery('simple', $1) + THEN 1.0 ELSE 0.0 END AS score FROM halachot h JOIN case_law cl ON cl.id = h.case_law_id WHERE {' AND '.join(halacha_filters)} - AND h.rule_tsv @@ plainto_tsquery('simple', $1) + AND (h.rule_tsv @@ plainto_tsquery('simple', $1) + OR cl.meta_tsv @@ plainto_tsquery('simple', $1)) ORDER BY score DESC LIMIT $2 """ @@ -3439,14 +3472,22 @@ async def search_precedent_library_lexical( parent.page_number AS parent_page_number, cl.case_number, cl.case_name, cl.court, cl.date AS decision_date, cl.precedent_level, cl.practice_area, cl.chair_name, cl.district, - ts_rank_cd(pc.content_tsv, plainto_tsquery('simple', $1)) AS score + GREATEST( + ts_rank_cd(pc.content_tsv, plainto_tsquery('simple', $1)), + ts_rank_cd(cl.meta_tsv, plainto_tsquery('simple', $1)) + ) + + CASE WHEN cl.meta_tsv @@ plainto_tsquery('simple', $1) + THEN 1.0 ELSE 0.0 END AS score FROM precedent_chunks pc JOIN case_law cl ON cl.id = pc.case_law_id LEFT JOIN precedent_chunks parent ON parent.id = pc.parent_chunk_id WHERE {' AND '.join(chunk_filters)} AND pc.embedding IS NOT NULL - AND pc.content_tsv @@ plainto_tsquery('simple', $1) + -- #55: exclude tiny fragment chunks (see semantic query above). + AND length(trim(pc.content)) >= 50 + AND (pc.content_tsv @@ plainto_tsquery('simple', $1) + OR cl.meta_tsv @@ plainto_tsquery('simple', $1)) ORDER BY score DESC LIMIT $2 """ diff --git a/mcp-server/src/legal_mcp/services/precedent_library.py b/mcp-server/src/legal_mcp/services/precedent_library.py index ee6dddc..43958c2 100644 --- a/mcp-server/src/legal_mcp/services/precedent_library.py +++ b/mcp-server/src/legal_mcp/services/precedent_library.py @@ -533,6 +533,7 @@ async def list_precedents( precedent_level: str = "", source_type: str = "", search: str = "", + source_kind: str = "external_upload", limit: int = 100, offset: int = 0, ) -> list[dict]: @@ -542,6 +543,7 @@ async def list_precedents( precedent_level=precedent_level, source_type=source_type, search=search, + source_kind=source_kind, limit=limit, offset=offset, ) diff --git a/mcp-server/src/legal_mcp/tools/precedent_library.py b/mcp-server/src/legal_mcp/tools/precedent_library.py index 2e869b0..a2855db 100644 --- a/mcp-server/src/legal_mcp/tools/precedent_library.py +++ b/mcp-server/src/legal_mcp/tools/precedent_library.py @@ -103,6 +103,7 @@ async def precedent_library_list( precedent_level: str = "", source_type: str = "", search: str = "", + source_kind: str = "external_upload", limit: int = 100, ) -> str: """רשימה של פסיקה בקורפוס הסמכותי, עם פילטרים.""" @@ -112,6 +113,7 @@ async def precedent_library_list( precedent_level=precedent_level, source_type=source_type, search=search, + source_kind=source_kind, limit=limit, ) return _ok(rows) diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index d7e99a8..2aa5044 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -12,6 +12,7 @@ | `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס | | `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — הסקריפט מדלג על הסוכן עם warning. בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות לפני sync.** | ידני אחרי כל שינוי | | `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) | +| `test_retrieval_by_name.py` | python | בדיקת אחזור-לפי-שם (#52/RC-A) — מאמת ש`search_precedent_library`/`search_internal_decisions` מדרגים את ההחלטה עצמה (אגסי) מעל מי שמצטט אותה, + רגרסיות לשאילתות מהותיות. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/test_retrieval_by_name.py` (exit 0 = עבר). | ידני אחרי שינוי שכבת חיפוש | | `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) | | `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` | | `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני | diff --git a/scripts/test_retrieval_by_name.py b/scripts/test_retrieval_by_name.py new file mode 100644 index 0000000..8e136d0 --- /dev/null +++ b/scripts/test_retrieval_by_name.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +"""Repro + regression test for retrieval-by-name (RC-A, tasks #52). + +Bug: searching the precedent corpus by a bare case NAME ("אגסי") fails to +surface the decision itself, because the lexical tsvector covers only chunk +content + halacha text — not case_name / case_number. A name query therefore +matches decisions that *cite* the case, not the case. + +Run with the MCP venv: + DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \ + mcp-server/.venv/bin/python scripts/test_retrieval_by_name.py + +Exit 0 = all assertions pass. Non-zero = failure (prints what was found). +""" +import asyncio +import sys + +sys.path.insert(0, "/home/chaim/legal-ai/mcp-server/src") + +from legal_mcp.services import embeddings, hybrid_search # noqa: E402 + +AGASI_ID = "1a87efe5-6e13-4ed4-a9ec-3f2f7d61e4ec" +# Vinfeld CITES Agasi (its halacha quote names אגסי) but is NOT Agasi. +# An exact name match must rank the case itself above any case citing it. +VINFELD_ID = "bd5d849c-c15f-43c3-96ab-d44337af9cb5" +NAME_QUERY = "אגסי" +SUBSTANTIVE_QUERY = 'פטור היטל השבחה לפי סעיף 19(ג)(1) שתי דירות 140 מ"ר אחת מושכרת' + + +def _ids(rows): + return [str(r.get("case_law_id")) for r in rows] + + +def _rank_of(rows, cid): + for i, r in enumerate(rows, 1): + if str(r.get("case_law_id")) == cid: + return i + return None + + +async def _search(query, source_kind, limit=10): + query_emb = await embeddings.embed_query(query) + return await hybrid_search.search_precedent_library_hybrid( + query, + query_emb, + source_kind=source_kind, + limit=limit, + include_halachot=True, + ) + + +async def main(): + results = {"pass": [], "fail": []} + + # 1) THE BUG: bare-name query must rank the case ITSELF (Agasi) above any + # case that merely CITES it (Vinfeld), and within the top 3. + rows = await _search(NAME_QUERY, "internal_committee", limit=10) + a_rank = _rank_of(rows, AGASI_ID) + v_rank = _rank_of(rows, VINFELD_ID) + ok = bool(a_rank) and a_rank <= 3 and (v_rank is None or a_rank < v_rank) + msg = (f"[name/internal] query='{NAME_QUERY}' -> Agasi rank={a_rank}, " + f"Vinfeld(citer) rank={v_rank} (top ids: {_ids(rows)[:5]})") + (results["pass"] if ok else results["fail"]).append(msg) + + # 2) REGRESSION: substantive query must still find Agasi with a real score. + rows = await _search(SUBSTANTIVE_QUERY, "internal_committee", limit=10) + rank = _rank_of(rows, AGASI_ID) + top_score = float(rows[0]["score"]) if rows else 0.0 + msg = f"[substantive/internal] Agasi rank={rank}, top_score={top_score:.3f}" + (results["pass"] if rank and rank <= 8 else results["fail"]).append(msg) + + # 3) REGRESSION: substantive query in the full precedent library still works + # (Vinfeld/נווה שלום etc. should surface; just assert non-empty + has betterment content). + rows = await _search(SUBSTANTIVE_QUERY, "external_upload", limit=10) + msg = f"[substantive/external] returned {len(rows)} rows (top ids: {_ids(rows)[:3]})" + (results["pass"] if len(rows) >= 3 else results["fail"]).append(msg) + + print("\n=== PASS ===") + for m in results["pass"]: + print(" ✓", m) + print("=== FAIL ===") + for m in results["fail"]: + print(" ✗", m) + + return 1 if results["fail"] else 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main()))