From a62116a571dd51e47b3e67d914714736b2b86eb5 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 30 May 2026 21:52:40 +0000 Subject: [PATCH] docs(spec): FU-3 re-index on content change design (GAP-09) + close #61.2 not-applicable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .taskmaster/tasks/tasks.json | 4 +- ...2026-05-30-fu3-reindex-on-change-design.md | 113 ++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 1e30573..98e4640 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -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" } diff --git a/docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md b/docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md new file mode 100644 index 0000000..dc91bd0 --- /dev/null +++ b/docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md @@ -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.