WS7 of the workflow-redesign initiative required closing 7 known leaks along the way. Cross-code audit verified ALL 7 were already closed in prior cycles (FU-1/4/5/6/7 + IA waves); the spec still carried stale "known violation" text. This PR aligns the spec to actual code state and verifies each closure with a test. No new code path created — this is a spec-alignment audit, not a re-solve. 7 leaks → status (all closed/gated, each with a test): 1. cross-corpus halachot (#56, GAP-10/FU-4) — cl.source_kind in halacha_filters (db.py:7516,7519); test_precedent_corpus_isolation.py 2. invisible halacha backlog (GAP-14/FU-5) — health halacha_backlog (app.py:2352-2364) + extraction_status/halachot_pending 3. no eval harness (GAP-11/FU-5, G8) — scripts/eval_retrieval.py (P/R/MRR/nDCG vs gold-set+baseline); --self-test ALL PASS 4. export gate not hard-block (GAP-15/FU-6, INV-EX3) — export_docx checks qa_run_exists + get_critical_qa_failures before exporter (drafting.py:462-494); test_export_qa_gate.py 5. metadata on internal path (GAP-02/FU-1, INV-ING3) — unified ingest.ingest_document queues metadata+halacha together (ingest.py:233-234); test_unified_ingest.py 6. DOCX creep to source-of-truth (GAP-17/FU-7, INV-EX1) — active_draft_path is revision-anchor only; drift caught by cases.blocks_stale flag (V22) 7. UI cache-invalidation (GAP-33/FU-10) — qc.invalidateQueries on mutations Also added forward-pointer notes for the workflow-redesign additions owned by sibling tasks (02 documents is_primary/doc_category → #200; 04 re-analysis / summarize_party_claims / analyze_protocol → #200/#202/#203), kept additive to avoid line conflicts. block-schema.md interim/template edits deferred to #204/#205. Invariants: maintains G2 (single source of truth, no parallel path — all leak closures extend the canonical path), G8 (eval harness now exists), G10 (human gates visible: halacha backlog + hard export gate), INV-ING3 (unified metadata queue), INV-EX1 (DOCX is derived; blocks_stale drift flag), INV-RET1/G5 (#56 cross-corpus halacha isolation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 KiB
03 — אחזור (Retrieval: Corpora · Hybrid/RRF · Attribution · Eval)
קובץ-תחום זה כפוף ל-חוקת המערכת ומגדיר את שכבת-האחזור הקנונית (TARGET) — שלושת הקורפוסים, כלי-החיפוש המכוונים לכל אחד, מנגנון ה-hybrid (dense + lexical) ומיזוג ה-RRF, עקיבוּת-המקור והרמוניית-המדידה. הוא אוכף את G4 (חוזה-שלמות לפני "ניתן-לחיפוש"), G5 (הפרדת-קורפוס בכל query), G6 (re-index), G7 (מיזוג RRF), G8 (eval) ו- G9 (עקיבוּת-מקור).
TARGET, לא תיאור-מצב. כל מקום שבו הקוד בפועל סוטה מהיעד מתועד כ-audit-finding (§5), תסמין לתיקון — לא התנהגות תקינה. כל טענה על הקוד מצוטטת
file:line.
כשל-השורש שהקובץ מייבש: 3 קורפוסים שחולקים תשתית-אחזור אחת, אך הפרדת-הקורפוס נאכפת רק על חלק ממסלולי-ה-query — כך שפריט מקורפוס אחד דולף לתוצאה של חיפוש בקורפוס אחר (cross-corpus leak).
1. שלושת הקורפוסים וכלי-החיפוש
| קורפוס | טבלת-אחסון | source_kind |
כלי-MCP מכוון | אימות file:line |
|---|---|---|---|---|
| מסמכי-תיק + קורפוס-סגנון דפנה | document_chunks |
— (מובחן ב-case_id/practice_area) |
search_decisions · search_case_documents · find_similar_cases |
search.py:15,91,145 → hybrid_search.py:41 (search_documents_hybrid) → db.search_similar (hybrid_search.py:56) |
| פסיקה חיצונית סמכותית | case_law + precedent_chunks/halachot |
external_upload |
search_precedent_library |
search.py→precedent_library.py:235 → search_library → hybrid_search.py:89,101 (source_kind="external_upload") |
| החלטות ועדות-ערר (פנימי) | case_law + precedent_chunks/halachot |
internal_committee |
search_internal_decisions |
search.py:228 → internal_decisions.py:395,411-418 (source_kind="internal_committee") → hybrid_search.py:89 |
הבחנת-שם קריטית (לא קורפוס רביעי): precedent_search_library (server.py:160) הוא כלי שונה —
מחפש בציטוטים שהיו"ר צירפה ידנית לתיקים (case_precedents), לא בקורפוס הפסיקה הסמכותית.
search_precedent_library (server.py:280) הוא הכלי לקורפוס החיצוני. אל תבלבל ביניהם.
הקורפוס החיצוני והפנימי חולקים טבלה אחת (case_law), מובחנים ב-source_kind בלבד
(02-data-model §2א). שניהם רצים דרך אותן פונקציות-DB
(search_precedent_library_semantic/_lexical) — לכן הפרדת-הקורפוס היא תנאי-סינון בתוך אותה שאילתה,
ושם נולדת ההפרה ב-§5.
שכבת-גילוי — יומונים, לא קורפוס-ציטוט. מעל 3 הקורפוסים יושבת שכבת-radar נפרדת: יומונים (סיכומי עפר-טויסטר), בטבלה פיזית נפרדת
digestsעם כליsearch_digests. היומון הוא מקור משני המצביע על הפסק המקורי — אינו קורפוס-ציטוט רביעי, אינו עקיב-בפלט (INV-RET5), ואינו נוגע ב-case_law/document_chunks. ההפרדה כאן פיזית (טבלה נפרדת), לא תנאי-סינון — ולכן INV-RET1 מתקיים טריוויאלית. מלא ב- X12-digests-radar.md (INV-DIG1–DIG3).
2. עיצוב ה-hybrid retrieval
לכל קורפוס שני retrievers הטרוגניים המאוחים ב-RRF, ולא בסכום-ציונים — ראה INV-RET3:
- Dense (semantic) — דמיון-קוסינוס מול
embedding vector(1024)(voyage). פסיקה:search_precedent_library_semantic(db.py:3143); מסמכי-תיק:db.search_similar. - Lexical (BM25-style) —
ts_rank_cdמולcontent_tsv/rule_tsv/meta_tsv(Postgres FTS). פסיקה:search_precedent_library_lexical(db.py:3366). מופעל כש-BM25_HYBRID_ENABLED(hybrid_search.py:139). - מיזוג sem+lex —
_merge_sem_lex(hybrid_search.py:240-308), נוסחתrrf_score = 1/(k+sem_rank) + 1/(k+lex_rank)(hybrid_search.py:256). - שכבת-multimodal (אופציונלית) — כש-
MULTIMODAL_ENABLED, עמודי-תמונה (voyage-multimodal-3) מאוחים לטקסט ב-RRF נפרד:_merge(hybrid_search.py:311-389),text_weight/(k+rank) + img_weight/(k+rank)(hybrid_search.py:356-357). - Diversity cap (MMR-style) —
_diversify_by_case_law(hybrid_search.py:196-225): לכל היותרmax_per_case_lawhits לכלcase_law_id, כדי שפסק-דין יחיד לא ישתלט על הרשימה.
למה RRF ולא סכום משוקלל: קוסינוס (~0.4–0.7) ו-
ts_rank_cd(~0.001–0.5, תלוי-אורך-שאילתה) חיים בסקיילים שונים — סכום משוקלל היה נותן לצד אחד להשתלט במקרה. RRF מאחד לפי דירוג, ולכן עמיד להבדלי-סקייל (hybrid_search.py:248-252,319-323). תואם feedback קיים (RRF, לא weighted-sum).
3. Invariants של התחום
INV-RET1: הפרדת-קורפוס נאכפת ב-100% ממסלולי-ה-query
כלל: הפרדת 3 הקורפוסים נאכפת בכל מסלול-אחזור — גם בסינון ה-chunks וגם בסינון ההלכות.
אין פריט מקורפוס אחד שמופיע בתוצאת חיפוש שכוון לקורפוס אחר. כל ענף-SQL (semantic/lexical,
chunks/halachot) נושא את אותו תנאי-source_kind.
מקורות: Pinecone — Implement multitenancy (metadata-filter isolation per tenant) · RAG
attribution (Lewis et al., 2020, NeurIPS — pinned non-leaking provenance) · ISO 8000 (Data
quality / conformance) | סטטוס: verified
אכיפה: תנאי-source_kind בכל ענף-SQL בשכבת-החיפוש; בדיקת-בריאות שמריצה שאילתת-ביקורת
(חיפוש מכוון-קורפוס שמחזיר פריט בעל source_kind זר = כשל). אוכף את
G5.
הפרה ידועה — ✅ נסגרה (FU-4, GAP-10, משימה #56): בעבר halacha_filters לא כללו
cl.source_kind (בניגוד ל-chunk_filters שכן) → הלכות מהקורפוס הפנימי דלפו לחיפוש בקורפוס
החיצוני ולהפך. נסגר: שני ה-halacha_filters (semantic + lexical) מתחילים כעת ב-
cl.source_kind = '{source_kind}' בדיוק כמו chunk_filters (db.py:7516,7519 ומקבילו ב-
search_precedent_library_lexical). מאומת ב-test_precedent_corpus_isolation.py (semantic+lexical,
שני ה-source_kind) → תיעוד-מקור: audit.
INV-RET2: אין החזרה/אינדוקס בלי metadata מלא + locator פתיר
כלל: פריט אינו מוחזר מ-search (ואינו נחשף לאחזור) אלא אם שדות-החובה שלו מולאו
(G4) ובידו locator פתיר למקור
(case_law_id/document_id + מזהה-עמוד/chunk). רשומה ללא metadata לא-ריק או ללא chunk עם
embedding מסומנת searchable=false ולא מוחזרת (02-data-model INV-DM1).
מקורות: Pinecone (metadata filtering — completeness לפני שליפה) · RAG attribution (Lewis et
al., 2020) · ISO 8000 (completeness) | סטטוס: verified
אכיפה: חוזה-שלמות בנקודת-הקליטה (02-data-model §2)
- סינון בשכבת-החיפוש (
embedding IS NOT NULL,db.py:3239,3271;length(trim(content))>=50,db.py:3274) + בדיקת-בריאות שחושפת backlog. אוכף את G5. הפרה ידועה: ערן סופר 8046/24 — נקלטה בלי metadata (headnote/summary/tags ריקים), היעדר תיזמון חילוץ-מטא-דאטה במסלול הפנימי (01-ingest INV-ING3), אך ללא דגל-searchableמפורש שימנע את חשיפתה לאחזור → ממצא ל-audit.
INV-RET3: מיזוג retrievers הטרוגניים ב-RRF בלבד
כלל: מיזוג תוצאות בין retrievers שונים (semantic↔lexical, text↔image) נעשה אך ורק
לפי דירוג (Reciprocal Rank Fusion) — לעולם לא סכום/ממוצע ציונים גולמיים, שכן ציונים בסקיילים
שונים אינם בני-השוואה ישירה.
מקורות: Elastic — Reciprocal Rank Fusion · Weaviate — Hybrid Search Explained · Manning,
Raghavan & Schütze, Introduction to Information Retrieval (CUP, 2008) | סטטוס: verified
אכיפה: מיזוג sem+lex ב-_merge_sem_lex (hybrid_search.py:240-308, נוסחה ב-:256) ומיזוג
text+image ב-_merge (hybrid_search.py:311-389, נוסחה ב-:356-357), שניהם עם
k = MULTIMODAL_RRF_K. אוכף את G7.
מצב: כבר ממומש (codify, לא gap) — הקוד הקיים מיישם RRF נכון בשני המיזוגים. ה-invariant
מקבע את ההתנהגות הקיימת כחוזה. הפרה ידועה: —
INV-RET4: איכות-אחזור נמדדת ב-eval harness עומד (precision + recall)
כלל: איכות-האחזור נמדדת אמפירית — precision ו-recall — מול סט-שאילתות מתויג קבוע
(labeled query set) ב-eval harness עומד. כל שינוי בשכבת-האחזור (משקלי-RRF, k, סף-chunk, embedder)
מלווה במדידה לפני/אחרי; אין כוונון "לפי תחושה".
מקורות: Manning, Raghavan & Schütze, Introduction to Information Retrieval (CUP, 2008 — fixed
relevance judgments, precision/recall) · RAG evaluation literature (Lewis et al., 2020 ואחריו) ·
Elastic — relevance evaluation guidance | סטטוס: verified
אכיפה: eval harness עם gold-set מתויג + בדיקת-בריאות תקופתית; שער-CI על שינוי שכבת-האחזור.
אוכף את G8.
הפרה ידועה — ✅ נסגרה (FU-5, GAP-11): בעבר היה רק telemetry.log_search_bg (תצפית, לא
הערכה). נסגר: scripts/eval_retrieval.py מריץ את מסלול-האחזור בייצור (אותן פונקציות-
שירות שכלי-ה-MCP קוראים) מול gold-set מתויג (data/eval/gold-set.jsonl, נבנה ב-
scripts/eval_gold_bootstrap.py) ומחזיר precision/recall/MRR/nDCG@{5,10} — סך-הכול, פר-קורפוס
ופר-practice_area — מול baseline מקובע (data/eval/baseline.json). מטריקות-הליבה נבדקות
offline ב-eval_retrieval.py --self-test ("ALL PASS"). משמעת-CI: להריץ לפני/אחרי כל שינוי
שכבת-אחזור (משקלי-RRF/k/סף-chunk/embedder/rerank) ולהשוות ל-baseline. תיעוד-מקור: audit.
INV-RET5: כל span מוחזר עקיב למקורו
כלל: כל קטע מוחזר נושא עקיבוּת-מקור מלאה — מזהה-מסמך/פסק-דין (case_law_id/document_id/
case_number) ו-locator בתוכו (page_number / chunk_id / supporting_quote להלכה). פלט
ללא ייחוס פתיר אינו תקין; היו"ר חייבת לאמת כל ציטוט מול מקורו.
מקורות: Council of Europe / CEPEJ — European Ethical Charter on AI in judicial systems
(2018, traceability) · RAG attribution (Lewis et al., 2020) · ISO 15489-1:2016 (records
authenticity/integrity) | סטטוס: verified
אכיפה: כל פורמטר-תוצאה כולל מזהה + locator: search.py:77-86 (case_number/page/section),
_format_internal_row (search.py:322-343: case_number/case_name/court + content/page או
rule/quote להלכה). עקיבוּת מלאה מפורטת ב-X5-audit-provenance.md. אוכף את
G9.
הפרה ידועה: —
4. re-index ושינוי-תוכן (G6)
האחזור מסתמך על embeddings מסונכרנים מול התוכן. ה-tsvectors (content_tsv/rule_tsv/meta_tsv)
הם GENERATED ALWAYS … STORED (db.py:778,782,1086) ולכן מתעדכנים אוטומטית; אך ה-embedding vector(1024) אינו generated — הוא תלוי-טריגר-חיצוני, נקודת-drift אפשרית
(02-data-model INV-DM3). שינוי-תוכן חייב להפעיל
re-embed; בדיקת-בריאות מגלה embeddings מיושנים. אוכף את
G6.
5. מצב קיים מול יעד — audit-findings
ההבדלים בין הקוד בפועל ל-TARGET. אלו תסמינים, לא התנהגויות תקינות. כל פריט אומת מול הקוד.
- דליפת-הלכות חוצת-קורפוס (משימה #56) — ✅ נסגר (FU-4, GAP-10). בעבר
halacha_filtersלא כללוcl.source_kind(בניגוד ל-chunk_filters) → הלכה מהקורפוס הפנימי יכלה לחזור בחיפוש בקורפוס החיצוני, ולהפך. נסגר:halacha_filtersמתחילים כעת ב-cl.source_kind = '{source_kind}'cl.searchable = trueבדיוק כמוchunk_filters(db.py:7514-7519), בשתי הפונקציות (semantic+lexical). מאומת ב-test_precedent_corpus_isolation.py(INV-RET1).
- eval harness — ✅ נסגר (FU-5, GAP-11). בעבר רק
telemetry.log_search_bg(תצפית). נסגר:scripts/eval_retrieval.py(precision/recall/MRR/nDCG מולdata/eval/gold-set.jsonl- baseline;
--self-testעובר offline) — מדידה אמפירית מול תיוג קבוע (INV-RET4).
- baseline;
search_decisionsמתעד אזהרה כשאיןpractice_areaאך לא חוסם. ללא פילטר-תחום החיפוש עלול לערבב תחומים משפטיים (search.py:45-49,172-176—logger.warning, ממשיך). יעד: הפרדה לפי תחום נאכפת, לא מומלצת בלבד — תואם את עקרון ההפרדה ב-G5.embeddingאינוGENERATED(בניגוד ל-tsvector). נקודת-drift אפשרית בין תוכן ל-embedding אחרי עדכון (§4; תואם 02-data-model). יעד: טריגר re-embed מובטח + health-check.
6. הפניות-אחיות
- 00-constitution.md — invariants גלובליים (G4–G9) + כללי-הנדסה.
- 01-ingest.md — חוזה-הקליטה שמייצר את ה-chunks/embeddings שהאחזור שולף.
- 02-data-model.md — חוזה-השלמות (searchable) + re-index שהאחזור מסנן לפיהם.
- 05-qa-review.md — שער-הלכה הידני (
review_status) שמגדיר אילו הלכות searchable. - X5-audit-provenance.md — עקיבוּת-מקור מלאה של כל span מוחזר (בסיס ל-INV-RET5).
- X12-digests-radar.md — שכבת-הגילוי (יומונים) שמעל הקורפוסים — מצביעה, לא מצוטטת.