fix(retrieval): make decisions findable by name + unhide committee uploads
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m57s

Root cause of "agent can't find the Agasi decision in the corpus" (CMPA-55):
the decision was fully ingested, but the retrieval layer failed on the
realistic agent query — searching by case name.

- RC-A (#52): lexical tsvector covered only chunk content + halacha text,
  so a bare-name query ("אגסי") matched decisions that *cite* the case, not
  the case itself. Add meta_tsv on case_law(case_name, case_number) (SCHEMA
  V20) and OR it into the lexical halacha/chunk SQL with a match boost, so a
  name/number hit surfaces the case's own rows. Agasi: rank 4 → rank 1.
- RC-B (#53): precedent_library_list hard-defaulted source_kind=external_upload
  and never exposed the param, hiding uploaded ערר/בל"מ (internal_committee)
  decisions. Thread source_kind through service → tool → MCP tool (supports
  'internal_committee' / 'all_committees').
- #54: agent instructions (researcher/analyst/writer) — search-by-name
  protocol: add content/case-number, search both corpora, use all_committees
  before declaring "not in corpus".
- #55: chunker produced tiny fragment chunks ("דיון", "החלטה") from header
  keywords matched mid-sentence. Anchor SECTION_PATTERNS to line start +
  merge sub-min sections; exclude <50-char fragments at query time (484
  existing fragments hidden; full re-chunk tracked as #57).

Tests: scripts/test_retrieval_by_name.py (name ranks case above citer +
substantive regressions); chunker unit checks (0 tiny chunks). New findings
filed as tasks #56 (halacha source_kind leak) and #57 (re-chunk migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 11:26:19 +00:00
parent 165efc62b0
commit 58ab003206
11 changed files with 355 additions and 57 deletions

View File

@@ -455,12 +455,12 @@ X שאלות עומדות להכרעה:
### 8א. אימות פסיקה ### 8א. אימות פסיקה
סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו'). סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו').
לכל פסק דין שמוזכר: לכל פסק דין שמוזכר:
1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. 1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. הקורפוס כולל גם הלכות מהחלטות ועדות ערר שהועלו (internal_committee).
2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`) 2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`)
3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות 3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות
4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס. 4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס.
5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס". 5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס".
6. **אם לא נמצא בכלל** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש. 6. **אם לא נמצא בכלל** — קודם **נסה שוב עם הקשר** (לא שם לבדו): צרף מונחי תוכן או מספר תיק לשאילתה. שם תיק לבדו (`"אגסי"`) אינו מפתח אמין — הוא עלול להחזיר את מי שמצטט את התיק ולא את התיק עצמו. רק אם גם זה ריק — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש.
הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2. הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2.

View File

@@ -269,9 +269,18 @@ search_internal_decisions(
**מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו. **מינימום:** 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`) — חובה #### 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 ("טוענים שמופיע" ≠ "אומת") - ה-writer יודע שלא להסתמך על פסיקה שלא ב-DB ("טוענים שמופיע" ≠ "אומת")

View File

@@ -351,6 +351,8 @@ fi
חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד. חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד.
**איתור החלטה לפי שם:** אם אתה מחפש החלטה ספציפית בשמה (למשל "אגסי"), אל תחפש בשם לבדו — צרף מונחי תוכן או מספר תיק (`"אגסי 19(ג)(1) 140 מ"ר"` / `"ערר 81002-01-21"`). שאילתת-שם בלבד עלולה להחזיר את מי שמצטט את ההחלטה ולא את ההחלטה עצמה.
### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind` ### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind`
כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה: כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה:

View File

@@ -1388,50 +1388,59 @@
{ {
"id": 1, "id": 1,
"title": "Audit + migration practice_area (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197)", "title": "Audit + migration practice_area (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197)",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 2, "id": 2,
"title": "Audit + reclassify case_law source_kind external_upload → internal_committee עבור 'ערר' prefix", "title": "Audit + reclassify case_law source_kind external_upload → internal_committee עבור 'ערר' prefix",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 3, "id": 3,
"title": "Delete + re-extract halachot עבור רשומות שעברו reclassification", "title": "Delete + re-extract halachot עבור רשומות שעברו reclassification",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 4, "id": 4,
"title": "תיקון נתיב יצירת תיק לתיוג practice_area נכון מההתחלה", "title": "תיקון נתיב יצירת תיק לתיוג practice_area נכון מההתחלה",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 5, "id": 5,
"title": "תיקון /api/precedent-library/upload לניתוב לפי תחילית הציטוט", "title": "תיקון /api/precedent-library/upload לניתוב לפי תחילית הציטוט",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 6, "id": 6,
"title": "מבחני רגרסיה לכל 3 הbaגים", "title": "מבחני רגרסיה לכל 3 הbaגים",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 7, "id": 7,
"title": "תיקון MCP `case_update` + API `PUT /api/cases/{case_number}` לתמוך בעדכון practice_area + appeal_subtype", "title": "תיקון MCP `case_update` + API `PUT /api/cases/{case_number}` לתמוך בעדכון practice_area + appeal_subtype",
"status": "done", "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, "id": 8,
"title": "[prevention] DB CHECK constraints: source_kind='internal_committee' ⇒ chair_name NOT NULL; cases.practice_area enum", "title": "[prevention] DB CHECK constraints: source_kind='internal_committee' ⇒ chair_name NOT NULL; cases.practice_area enum",
"status": "done", "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, "id": 9,
"title": "[prevention] Unify practice_area taxonomy — מיפוי או מחיקה של appeals_committee מ-practice_area.py", "title": "[prevention] Unify practice_area taxonomy — מיפוי או מחיקה של appeals_committee מ-practice_area.py",
"status": "done", "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" "updatedAt": "2026-05-26T08:35:22.762800Z"
@@ -1452,19 +1461,22 @@
"id": 1, "id": 1,
"title": "Backfill chair_name + district לכל 7 הרשומות החסרות (LLM extraction)", "title": "Backfill chair_name + district לכל 7 הרשומות החסרות (LLM extraction)",
"status": "done", "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, "id": 2,
"title": "[prevention] Validation: chair_name+district required ב-internal_decisions_upload (API+MCP)", "title": "[prevention] Validation: chair_name+district required ב-internal_decisions_upload (API+MCP)",
"status": "done", "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, "id": 3,
"title": "[prevention] UI dropdown ל-district בטופס העלאת החלטות ועדה (web-ui)", "title": "[prevention] UI dropdown ל-district בטופס העלאת החלטות ועדה (web-ui)",
"status": "done", "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" "updatedAt": "2026-05-26T08:35:22.762800Z"
@@ -1521,32 +1533,38 @@
{ {
"id": 1, "id": 1,
"title": "Migration + model missing_precedents", "title": "Migration + model missing_precedents",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 2, "id": 2,
"title": "API endpoints POST/GET/upload/PATCH /api/missing-precedents", "title": "API endpoints POST/GET/upload/PATCH /api/missing-precedents",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 3, "id": 3,
"title": "MCP tools missing_precedent_create/list/close", "title": "MCP tools missing_precedent_create/list/close",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 4, "id": 4,
"title": "Next.js page /missing-precedents עם list + detail + upload form", "title": "Next.js page /missing-precedents עם list + detail + upload form",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 5, "id": 5,
"title": "Auto-creation hook במחקר (legal-researcher יוצר רשומה כשמזהה ציטוט חסר)", "title": "Auto-creation hook במחקר (legal-researcher יוצר רשומה כשמזהה ציטוט חסר)",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 6, "id": 6,
"title": "Webhook עדכון לפלאגין Paperclip + Comment לחיים", "title": "Webhook עדכון לפלאגין Paperclip + Comment לחיים",
"status": "done" "status": "done",
"parentId": "undefined"
} }
], ],
"updatedAt": "2026-05-26T08:35:22.762800Z" "updatedAt": "2026-05-26T08:35:22.762800Z"
@@ -1564,27 +1582,32 @@
{ {
"id": 1, "id": 1,
"title": "Migration + models legal_arguments + legal_argument_propositions", "title": "Migration + models legal_arguments + legal_argument_propositions",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 2, "id": 2,
"title": "LLM aggregation job (Hermes/DeepSeek profile)", "title": "LLM aggregation job (Hermes/DeepSeek profile)",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 3, "id": 3,
"title": "API + MCP tool aggregate_claims_to_arguments", "title": "API + MCP tool aggregate_claims_to_arguments",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 4, "id": 4,
"title": "UI display update — case detail page מציג טיעונים אמיתיים", "title": "UI display update — case detail page מציג טיעונים אמיתיים",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 5, "id": 5,
"title": "Backfill לכל התיקים הקיימים", "title": "Backfill לכל התיקים הקיימים",
"status": "done" "status": "done",
"parentId": "undefined"
} }
], ],
"updatedAt": "2026-05-26T08:35:22.762800Z" "updatedAt": "2026-05-26T08:35:22.762800Z"
@@ -1602,27 +1625,32 @@
{ {
"id": 1, "id": 1,
"title": "הוספת 3 ערכי enum ל-practice_area.py APPEALS_COMMITTEE_SUBTYPES", "title": "הוספת 3 ערכי enum ל-practice_area.py APPEALS_COMMITTEE_SUBTYPES",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 2, "id": 2,
"title": "כתיבת 3 templates מתודולוגיים ב-docs/methodology/extension-request-{type}.md", "title": "כתיבת 3 templates מתודולוגיים ב-docs/methodology/extension-request-{type}.md",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 3, "id": 3,
"title": "אוטו-זיהוי בקוד יצירת תיק (subject='בקשה להארכת מועד' → קביעת subtype לפי practice_area)", "title": "אוטו-זיהוי בקוד יצירת תיק (subject='בקשה להארכת מועד' → קביעת subtype לפי practice_area)",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 4, "id": 4,
"title": "UI badge + filter ייעודי לבל\"מ", "title": "UI badge + filter ייעודי לבל\"מ",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 5, "id": 5,
"title": "עדכון web/paperclip_client.py mapping ל-company עבור 3 הערכים החדשים", "title": "עדכון web/paperclip_client.py mapping ל-company עבור 3 הערכים החדשים",
"status": "done" "status": "done",
"parentId": "undefined"
} }
], ],
"updatedAt": "2026-05-26T08:35:22.762800Z" "updatedAt": "2026-05-26T08:35:22.762800Z"
@@ -1647,55 +1675,65 @@
{ {
"id": 1, "id": 1,
"title": "עדכון .claude/agents/legal-ceo.md — routing + statuses + wake reasons", "title": "עדכון .claude/agents/legal-ceo.md — routing + statuses + wake reasons",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 2, "id": 2,
"title": "עדכון .claude/agents/legal-analyst.md — practice_area, legal_arguments, בל\"מ detection", "title": "עדכון .claude/agents/legal-analyst.md — practice_area, legal_arguments, בל\"מ detection",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 3, "id": 3,
"title": "עדכון .claude/agents/legal-researcher.md — 2 layers, missing_precedents, citations, בל\"מ templates", "title": "עדכון .claude/agents/legal-researcher.md — 2 layers, missing_precedents, citations, בל\"מ templates",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 4, "id": 4,
"title": "עדכון .claude/agents/legal-writer.md — legal_arguments view, בל\"מ templates", "title": "עדכון .claude/agents/legal-writer.md — legal_arguments view, בל\"מ templates",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 5, "id": 5,
"title": "עדכון .claude/agents/legal-qa.md — בל\"מ-aware validation", "title": "עדכון .claude/agents/legal-qa.md — בל\"מ-aware validation",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 6, "id": 6,
"title": "עדכון .claude/agents/HEARTBEAT.md — כללי routing משותפים + research_complete status", "title": "עדכון .claude/agents/HEARTBEAT.md — כללי routing משותפים + research_complete status",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 7, "id": 7,
"title": "סנכרון לכל החברות CMPA mirror — sync_agents_across_companies.py", "title": "סנכרון לכל החברות CMPA mirror — sync_agents_across_companies.py",
"status": "done" "status": "done",
"parentId": "undefined"
}, },
{ {
"id": 8, "id": 8,
"title": "[alignment] researcher docs: דרישה מפורשת שכל 'ערר' → internal_decision_upload, לעולם לא precedent_library_upload", "title": "[alignment] researcher docs: דרישה מפורשת שכל 'ערר' → internal_decision_upload, לעולם לא precedent_library_upload",
"status": "done", "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, "id": 9,
"title": "[alignment] analyst docs: הסבר על 2 taxonomies של practice_area + מתי משתמשים בכל אחת", "title": "[alignment] analyst docs: הסבר על 2 taxonomies של practice_area + מתי משתמשים בכל אחת",
"status": "done", "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, "id": 10,
"title": "[alignment] writer docs: הבחנה בין source_kind בציטוט (binding vs persuasive)", "title": "[alignment] writer docs: הבחנה בין source_kind בציטוט (binding vs persuasive)",
"status": "done", "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" "updatedAt": "2026-05-26T07:41:47.880478Z"
@@ -1870,17 +1908,96 @@
"priority": "low", "priority": "low",
"subtasks": [], "subtasks": [],
"updatedAt": "2026-05-26T11:27:09.039154Z" "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": { "metadata": {
"version": "1.0.0", "version": "1.0.0",
"lastModified": "2026-05-04T17:29:25.687Z", "lastModified": "2026-05-30T11:19:23.923Z",
"taskCount": 29, "taskCount": 57,
"completedCount": 24, "completedCount": 52,
"tags": [ "tags": [
"legal-ai" "legal-ai"
], ]
"updated": "2026-05-26T06:39:31.733370"
} }
} }
} }

View File

@@ -201,11 +201,20 @@ async def precedent_library_list(
precedent_level: str = "", precedent_level: str = "",
source_type: str = "", source_type: str = "",
search: str = "", search: str = "",
source_kind: str = "external_upload",
limit: int = 100, limit: int = 100,
) -> str: ) -> 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( 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,
) )

View File

@@ -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) 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]]: 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 # Find all section headers and their positions
markers: list[tuple[int, str]] = [] markers: list[tuple[int, str]] = []
for pattern, section_type in SECTION_PATTERNS: 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)) markers.append((match.start(), section_type))
if not markers: if not markers:
@@ -120,11 +139,18 @@ def _split_into_sections(text: str) -> list[tuple[str, str]]:
if intro_text: if intro_text:
sections.append(("intro", 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): for i, (pos, section_type) in enumerate(markers):
end = markers[i + 1][0] if i + 1 < len(markers) else len(text) end = markers[i + 1][0] if i + 1 < len(markers) else len(text)
section_text = text[pos:end].strip() 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)) sections.append((section_type, section_text))
return sections return sections

View File

@@ -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 def _run_schema_migrations(pool: asyncpg.Pool) -> None:
async with pool.acquire() as conn: async with pool.acquire() as conn:
await conn.execute(SCHEMA_SQL) 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_V17_SQL)
await conn.execute(SCHEMA_V18_SQL) await conn.execute(SCHEMA_V18_SQL)
await conn.execute(SCHEMA_V19_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: async def init_schema() -> None:
@@ -3217,6 +3241,9 @@ async def search_precedent_library_semantic(
ON parent.id = pc.parent_chunk_id ON parent.id = pc.parent_chunk_id
WHERE {' AND '.join(chunk_filters)} WHERE {' AND '.join(chunk_filters)}
AND pc.embedding IS NOT NULL 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 ORDER BY pc.embedding <=> $1
LIMIT $2 LIMIT $2
""" """
@@ -3411,11 +3438,17 @@ async def search_precedent_library_lexical(
h.practice_areas, h.subject_tags, h.confidence, h.rule_type, 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.case_number, cl.case_name, cl.court, cl.date AS decision_date,
cl.precedent_level, cl.chair_name, cl.district, 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 FROM halachot h
JOIN case_law cl ON cl.id = h.case_law_id JOIN case_law cl ON cl.id = h.case_law_id
WHERE {' AND '.join(halacha_filters)} 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 ORDER BY score DESC
LIMIT $2 LIMIT $2
""" """
@@ -3439,14 +3472,22 @@ async def search_precedent_library_lexical(
parent.page_number AS parent_page_number, parent.page_number AS parent_page_number,
cl.case_number, cl.case_name, cl.court, cl.date AS decision_date, cl.case_number, cl.case_name, cl.court, cl.date AS decision_date,
cl.precedent_level, cl.practice_area, cl.chair_name, cl.district, 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 FROM precedent_chunks pc
JOIN case_law cl ON cl.id = pc.case_law_id JOIN case_law cl ON cl.id = pc.case_law_id
LEFT JOIN precedent_chunks parent LEFT JOIN precedent_chunks parent
ON parent.id = pc.parent_chunk_id ON parent.id = pc.parent_chunk_id
WHERE {' AND '.join(chunk_filters)} WHERE {' AND '.join(chunk_filters)}
AND pc.embedding IS NOT NULL 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 ORDER BY score DESC
LIMIT $2 LIMIT $2
""" """

View File

@@ -533,6 +533,7 @@ async def list_precedents(
precedent_level: str = "", precedent_level: str = "",
source_type: str = "", source_type: str = "",
search: str = "", search: str = "",
source_kind: str = "external_upload",
limit: int = 100, limit: int = 100,
offset: int = 0, offset: int = 0,
) -> list[dict]: ) -> list[dict]:
@@ -542,6 +543,7 @@ async def list_precedents(
precedent_level=precedent_level, precedent_level=precedent_level,
source_type=source_type, source_type=source_type,
search=search, search=search,
source_kind=source_kind,
limit=limit, limit=limit,
offset=offset, offset=offset,
) )

View File

@@ -103,6 +103,7 @@ async def precedent_library_list(
precedent_level: str = "", precedent_level: str = "",
source_type: str = "", source_type: str = "",
search: str = "", search: str = "",
source_kind: str = "external_upload",
limit: int = 100, limit: int = 100,
) -> str: ) -> str:
"""רשימה של פסיקה בקורפוס הסמכותי, עם פילטרים.""" """רשימה של פסיקה בקורפוס הסמכותי, עם פילטרים."""
@@ -112,6 +113,7 @@ async def precedent_library_list(
precedent_level=precedent_level, precedent_level=precedent_level,
source_type=source_type, source_type=source_type,
search=search, search=search,
source_kind=source_kind,
limit=limit, limit=limit,
) )
return _ok(rows) return _ok(rows)

View File

@@ -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_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.** | ידני אחרי כל שינוי | | `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 חוזר. | חד-פעמי (בוצע) | | `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) | | `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) |
| `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` | | `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` |
| `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני | | `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני |

View File

@@ -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()))