docs(spec): FU-3 re-index on content change design (GAP-09) + close #61.2 not-applicable

content_hash/indexed_hash change detection + reindex_case_law from stored
full_text (no re-OCR) + drift health-check. Verified vs 3+ sources (content-
hash change detection, RAG re-embed-on-edit). #61.2 multimodal backfill closed:
42 rows are text-ingested (document_id NULL, no source PDF) — page-images
impossible without a PDF to render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:52:40 +00:00
parent 63dc08c963
commit a62116a571
2 changed files with 115 additions and 2 deletions

View File

@@ -2128,8 +2128,8 @@
"dependencies": [
1
],
"details": "מקור: בדיקת DB 2026-05-30 (precedent_image_embeddings JOIN case_law). internal_committee: 14/56 עם page-images, 42 בלי. נגזר מ-GAP-02/FU-1 boundary discussion. לא פער-תקינות — שיפור multimodal coverage.",
"status": "pending",
"details": "מקור: בדיקת DB 2026-05-30 (precedent_image_embeddings JOIN case_law). internal_committee: 14/56 עם page-images, 42 בלי. נגזר מ-GAP-02/FU-1 boundary discussion. לא פער-תקינות — שיפור multimodal coverage. | CLOSED not-applicable 2026-05-30: כל 42 הרשומות document_id=NULL + אין PDF בדיסק; multimodal דורש רינדור PDF → בלתי-אפשרי לרשומות-טקסט. אם יועלה PDF — ingest רגיל מטפל.",
"status": "cancelled",
"testStrategy": "",
"parentId": "61"
}

View File

@@ -0,0 +1,113 @@
# FU-3 — Re-Index on Content Change — עיצוב
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
**מכסה:** GAP-09 · **מספק:** INV-DM3, INV-G6, INV-ING4 (freshness) · **משימה:** TaskMaster #61
**תלוי ב:** FU-1 (#59) · **סוג:** pure-code + backfill-hash זול (אפס re-embed בריצה רגילה)
**מיגרציה:** V23 additive (2 עמודות-hash) + backfill-hash דטרמיניסטי הפיך. אין re-embed המוני.
---
## 1. הבעיה (מאומת בקוד)
`embedding` אינו עמודת `GENERATED` (בניגוד ל-tsvectors שמתעדכנים אוטומטית בשינוי-תוכן). חילוץ
embedding דורש קריאת-API, ולכן אי-אפשר להפוך אותו ל-GENERATED. הממצא של מיפוי-הקוד:
- **re-ingest דרך `ingest_document` כבר מבצע re-index נכון** — `_chunk_embed_store` רץ ללא-תנאי
ו-`store_precedent_chunks(_hierarchical)` הן DELETE-then-INSERT. אז המסלול המלא תקין.
- **3 פערים אמיתיים:** (א) אין **גילוי שינוי-תוכן** (אין `content_hash`/`updated_at` ב-case_law);
(ב) אין **נקודת re-index עצמאית** — כדי להטמיע מחדש חייבים לקלוט מחדש את ה**קובץ**, אך רשומות
רבות (למשל 42 החלטות-ועדה) נקלטו מ-`text` בלי קובץ; (ג) אין **גילוי-drift** בין תוכן ל-embeddings.
**אכיפת INV-G6** ("re-index בכל שינוי-תוכן") כשהטמעה אינה GENERATED = **גילוי (hash) + כלי-reindex
מתוכן-שמור + health-check** — בדיוק כדפוס ה-drift של FU-7 (detect-don't-auto-magic).
## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)
| החלטה | נימוק | מקורות |
|-------|--------|--------|
| `content_hash` (SHA-256 של full_text) לגילוי-שינוי, לא timestamp | hash תוכן הוא הדפוס המומלץ כשאין timestamp מהימן; דטרמיניסטי + collision-safe | Hash-based change detection (DeepWiki); Andy Dote content-hash; moby#9391 |
| re-index **מ-full_text שמור**, לא מ-re-extract/re-OCR | OCR לא-דטרמיניסטי; להשתמש בטקסט השמור (תואם [[feedback_no_reocr_retrofit]]) | RAG re-embed-on-edit (Medium); particula incremental update |
| detect→re-embed **רק שהשתנה** (לא rebuild מלא) + health-check staleness | incremental sync; ניטור recall כשהאינדקס מתיישן | apxml RAG updates; Pinecone/Weaviate (gap-audit) |
| backfill = hash בלבד (לא re-embed) — רשומות קיימות כבר מוטמעות | זול, הפיך, אפס עלות-API; re-embed רק כשתוכן באמת השתנה | — (נגזר מהמצב: 80 רשומות כבר embedded) |
## 3. הקבצים
- **Modify** `services/db.py`: V23 (`content_hash`, `indexed_hash` ב-case_law); `_content_hash(text)`;
כתיבת `content_hash` בכניסת `create_external_case_law`/`create_internal_committee_decision`/`create_case`;
`mark_indexed(case_law_id)` (מעתיק content_hash→indexed_hash); `recompute_content_hashes()` (backfill);
`list_stale_case_law()` (drift query).
- **Modify** `services/ingest.py`: אחרי `_chunk_embed_store` המוצלח → `mark_indexed(case_law_id)`; הוספת
`reindex_case_law(case_law_id)` — טוען row, chunk+embed+store מ-full_text שמור, ואז `mark_indexed`.
- **Modify** `services/metrics.py`: חשיפת `stale_embedding_case_law` count.
- **Add** MCP tool `precedent_reindex(case_law_id)` (wrapper דק ל-`ingest.reindex_case_law`) — מאפשר
הפעלה ידנית; voyage-API בלבד (אין CLI/LLM → בטוח גם בקונטיינר).
- **Test** `tests/test_reindex_on_change.py` (חדש).
**גבול:** אין שינוי לחתימות ציבוריות. `reindex_case_law` הוא **תוסף**; המסלול הקיים לא משתנה.
## 4. content_hash + indexed_hash
- `_content_hash(text) -> str`: `hashlib.sha256(text.encode()).hexdigest()`; על `""`/None → `""`.
- `content_hash` = hash של ה-full_text **הנוכחי**, נכתב בכל כתיבת-row (ב-create_*; גבול-הכתיבה כמו נרמול FU-2a).
- `indexed_hash` = ה-hash שעליו נבנו ה-chunks/embeddings **הנוכחיים**, נכתב ב-`mark_indexed` אחרי
store מוצלח (ב-ingest + ב-reindex).
- **טרי** ⇔ `content_hash = indexed_hash`. **stale**`content_hash IS DISTINCT FROM indexed_hash`
(כולל indexed_hash=NULL = "מעולם לא הוטמע מהתוכן הזה").
## 5. `reindex_case_law(case_law_id)` (GAP-09 enforcement)
```
load case_law row → full_text (שמור)
→ _chunk_embed_store(case_law_id, full_text, page_offsets=None, ...) # אותו מסלול קנוני
→ mark_indexed(case_law_id) # indexed_hash = content_hash
return {chunks, reindexed: true}
```
- **לא** קורא ל-extractor/OCR ולא ל-LLM — רק chunk (טקסט שמור) + embed (voyage) + store. תואם
[[feedback_no_reocr_retrofit]] ו-claude_session (אין CLI).
- multimodal: מדלג (page-images דורשים PDF; רשומות-טקסט אין להן — ראה §7). אם בעתיד יש קובץ — המסלול
המלא של ingest מטפל.
- idempotent (store = DELETE-then-INSERT; mark_indexed דטרמיניסטי).
## 6. גילוי-drift + health-check
- `list_stale_case_law()` → רשומות עם full_text לא-ריק ו-`content_hash IS DISTINCT FROM indexed_hash`.
- health-check (metrics.py) חושף `stale_embedding_case_law` count (INV-G6 observability; אחות ל-
`non_searchable_case_law`/`cases_with_stale_blocks` מ-FU-2a/FU-7).
## 7. #61.2 (multimodal backfill) — נסגר כלא-ישים
בדיקת-DB (2026-05-30): 42 החלטות-ועדה ללא page-images — **כולן** `document_id=NULL` ו-full_text
קיים, ואין PDF מקור בדיסק (`data/internal-decisions/` מכיל קובץ אחד). page-images דורשים **רינדור
PDF**; לרשומות-טקסט אין PDF → **בלתי-אפשרי**. לכן #61.2 נסגר כ-not-applicable. (אם יועלה PDF לאחת —
מסלול-ה-ingest הרגיל יטפל ב-multimodal.) FU-3 core מטמיע-מחדש את ה**טקסט** של כל 42 במידת-הצורך.
## 8. שינויי-התנהגות וסיכון
| שינוי | השפעה | סיכון |
|--------|--------|--------|
| content_hash בכתיבה | כל קליטה חדשה נושאת hash; טרי-מעצם-הקליטה | נמוך — additive |
| mark_indexed ב-ingest | רשומות חדשות = טרי (content=indexed) | נמוך |
| reindex_case_law | re-embed מתוכן שמור; עלות-API לפי-בקשה | נמוך — תוסף, ידני/מבוקר; לא רץ אוטומטית בהמוני |
| backfill hashes | content_hash לכולם; indexed_hash=content רק אם יש chunks, אחרת NULL | נמוך — הפיך, אפס re-embed |
| health-check stale count | חשיפת drift | נמוך — read-only |
## 9. אסטרטגיית בדיקה
`tests/test_reindex_on_change.py` — offline, monkeypatch. מקרים:
1. `_content_hash`: דטרמיניסטי; `""`/None→`""`; טקסט שונה→hash שונה.
2. stale-predicate: content≠indexed → stale; שווים → טרי; indexed=NULL → stale.
3. `mark_indexed` מריץ UPDATE שמעתיק content_hash→indexed_hash (monkeypatch conn).
4. `reindex_case_law`: טוען full_text, קורא _chunk_embed_store ו-mark_indexed (monkeypatch), לא קורא extractor/LLM.
5. create_* כותב content_hash (monkeypatch — assert ה-hash מועבר ל-INSERT/upsert).
> בדיקות-DB אמיתיות (V23, backfill, drift query) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a/FU-7.
## 10. סדר-ביצוע
1. בדיקות אדומות.
2. V23 (`content_hash`,`indexed_hash`) + `_content_hash` + `mark_indexed` + כתיבת content_hash ב-create_*.
3. `reindex_case_law` ב-ingest.py + קריאת `mark_indexed` אחרי `_chunk_embed_store` בקליטה.
4. `list_stale_case_law` + health-check `stale_embedding_case_law`.
5. MCP tool `precedent_reindex`.
6. backfill (DB smoke): `recompute_content_hashes()` — content_hash לכולם, indexed_hash=content אם יש chunks.
7. בדיקות ירוקות + smoke מול DB + lint + סגירת #61.2 + TaskMaster #61.