Files
legal-ai/docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md
Chaim a62116a571 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>
2026-05-30 21:52:40 +00:00

8.3 KiB
Raw Blame History

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. stalecontent_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.