Files
legal-ai/docs/spec/03-retrieval.md
Chaim 9ef0547997
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
docs(spec): WS7 — reconcile 01/02/03/04/06 + gap-audit; verify 7 workflow-redesign leaks closed (#207)
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>
2026-06-30 11:22:11 +00:00

190 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 — אחזור (Retrieval: Corpora · Hybrid/RRF · Attribution · Eval)
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **שכבת-האחזור הקנונית (TARGET)**
שלושת הקורפוסים, כלי-החיפוש המכוונים לכל אחד, מנגנון ה-hybrid (dense + lexical) ומיזוג ה-RRF,
עקיבוּת-המקור והרמוניית-המדידה. הוא אוכף את
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (חוזה-שלמות לפני "ניתן-לחיפוש"),
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query) (הפרדת-קורפוס בכל query),
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) (re-index),
[G7](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים) (מיזוג RRF),
[G8](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall) (eval) ו-
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (עקיבוּת-מקור).
> **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א](02-data-model.md#2א-case_law--החוזה-הקונקרטי)). שניהם רצים דרך **אותן** פונקציות-DB
(`search_precedent_library_semantic`/`_lexical`) — לכן הפרדת-הקורפוס היא **תנאי-סינון בתוך אותה שאילתה**,
ושם נולדת ההפרה ב-§5.
> **שכבת-גילוי — יומונים, לא קורפוס-ציטוט.** מעל 3 הקורפוסים יושבת שכבת-radar נפרדת: **יומונים**
> (סיכומי עפר-טויסטר), בטבלה פיזית נפרדת `digests` עם כלי `search_digests`. היומון הוא **מקור משני
> המצביע** על הפסק המקורי — **אינו** קורפוס-ציטוט רביעי, **אינו** עקיב-בפלט ([INV-RET5](#inv-ret5-כל-span-מוחזר-עקיב-למקורו)),
> ו**אינו** נוגע ב-`case_law`/`document_chunks`. ההפרדה כאן **פיזית** (טבלה נפרדת), לא תנאי-סינון —
> ולכן [INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query) מתקיים טריוויאלית. מלא ב-
> [X12-digests-radar.md](X12-digests-radar.md) (INV-DIG1DIG3).
---
## 2. עיצוב ה-hybrid retrieval
לכל קורפוס שני retrievers הטרוגניים המאוחים ב-RRF, ולא בסכום-ציונים — ראה [INV-RET3](#inv-ret3-מיזוג-retrievers-הטרוגניים-ב-rrf-בלבד):
1. **Dense (semantic)** — דמיון-קוסינוס מול `embedding vector(1024)` (voyage). פסיקה:
`search_precedent_library_semantic` (`db.py:3143`); מסמכי-תיק: `db.search_similar`.
2. **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`).
3. **מיזוג sem+lex**`_merge_sem_lex` (`hybrid_search.py:240-308`), נוסחת
`rrf_score = 1/(k+sem_rank) + 1/(k+lex_rank)` (`hybrid_search.py:256`).
4. **שכבת-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`).
5. **Diversity cap (MMR-style)**`_diversify_by_case_law` (`hybrid_search.py:196-225`): לכל היותר
`max_per_case_law` hits לכל `case_law_id`, כדי שפסק-דין יחיד לא ישתלט על הרשימה.
> **למה RRF ולא סכום משוקלל:** קוסינוס (~0.40.7) ו-`ts_rank_cd` (~0.0010.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](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
**הפרה ידועה — ✅ נסגרה (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](../audit-report.md).
### INV-RET2: אין החזרה/אינדוקס בלי metadata מלא + locator פתיר
**כלל:** פריט אינו מוחזר מ-search (ואינו נחשף לאחזור) אלא אם **שדות-החובה שלו מולאו**
([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)) **ובידו locator פתיר למקור**
(`case_law_id`/`document_id` + מזהה-עמוד/chunk). רשומה ללא metadata לא-ריק או ללא chunk עם
embedding מסומנת `searchable=false` ולא מוחזרת ([02-data-model INV-DM1](02-data-model.md#inv-dm1-searchable-רק-כשחוזה-השלמות-מתקיים)).
**מקורות:** Pinecone (metadata filtering — completeness לפני שליפה) · RAG attribution (Lewis et
al., 2020) · ISO 8000 (completeness) | סטטוס: verified
**אכיפה:** חוזה-שלמות בנקודת-הקליטה ([02-data-model §2](02-data-model.md#2-חוזה-שלמות-לכל-ישות-completeness-contract))
+ סינון בשכבת-החיפוש (`embedding IS NOT NULL`, `db.py:3239,3271`; `length(trim(content))>=50`,
`db.py:3274`) + בדיקת-בריאות שחושפת backlog. אוכף את
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
**הפרה ידועה:** ערן סופר 8046/24 — נקלטה בלי metadata (headnote/summary/tags ריקים), היעדר
תיזמון חילוץ-מטא-דאטה במסלול הפנימי ([01-ingest INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)),
אך ללא דגל-`searchable` מפורש שימנע את חשיפתה לאחזור → ממצא ל-[audit](../audit-report.md).
### 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](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים).
**מצב:** **כבר ממומש** (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](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall).
**הפרה ידועה — ✅ נסגרה (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](../audit-report.md).
### 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](X5-audit-provenance.md). אוכף את
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
**הפרה ידועה:**
---
## 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](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)). שינוי-תוכן חייב להפעיל
re-embed; בדיקת-בריאות מגלה embeddings מיושנים. אוכף את
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
---
## 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](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query)).
- **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](#inv-ret4-איכות-אחזור-נמדדת-ב-eval-harness-עומד-precision--recall)).
- **`search_decisions` מתעד אזהרה כשאין `practice_area` אך לא חוסם.** ללא פילטר-תחום החיפוש
עלול לערבב תחומים משפטיים (`search.py:45-49,172-176``logger.warning`, ממשיך). **יעד:** הפרדה
לפי תחום נאכפת, לא מומלצת בלבד — תואם את עקרון ההפרדה ב-[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
- **`embedding` אינו `GENERATED` (בניגוד ל-tsvector).** נקודת-drift אפשרית בין תוכן ל-embedding
אחרי עדכון ([§4](#4-re-index-ושינוי-תוכן-g6); תואם [02-data-model](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)).
**יעד:** טריגר re-embed מובטח + health-check.
---
## 6. הפניות-אחיות
- [00-constitution.md](00-constitution.md) — invariants גלובליים (G4G9) + כללי-הנדסה.
- [01-ingest.md](01-ingest.md) — חוזה-הקליטה שמייצר את ה-chunks/embeddings שהאחזור שולף.
- [02-data-model.md](02-data-model.md) — חוזה-השלמות (searchable) + re-index שהאחזור מסנן לפיהם.
- [05-qa-review.md](05-qa-review.md) — שער-הלכה הידני (`review_status`) שמגדיר אילו הלכות searchable.
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור מלאה של כל span מוחזר (בסיס ל-INV-RET5).
- [X12-digests-radar.md](X12-digests-radar.md) — שכבת-הגילוי (יומונים) שמעל הקורפוסים — מצביעה, לא מצוטטת.