# FU-7 — Audit-Trail + Provenance — עיצוב

**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
**מכסה:** GAP-17, GAP-18, GAP-19, GAP-20 · **מספק:** INV-AUD1, INV-AUD2, INV-AUD3, INV-EX1, INV-G9
**מקורות:** [X5-audit-provenance.md](../../spec/X5-audit-provenance.md), [06-export.md](../../spec/06-export.md), [gap-audit.md](../../spec/gap-audit.md)
**משימה:** TaskMaster #65 · **תלוי ב:** FU-1 (#59) · **סוג:** pure-code (schema-additive קל)
**מיגרציה:** אין. כל השינויים forward-only; backfill קל אופציונלי (provenance של בלוקים קיימים לא נאכף רטרואקטיבית).

---

## 1. מטרה והיקף

X5 §4 קובע את המנגנון הקנוני: **שימוש חוזר ב-`audit_log.log_action` עם `details` JSONB** —
לא טבלה חדשה (כלל-הנדסה "סימטריה"). FU-7 ממיר את `audit_log` מ"כמעט-ריק" ל-audit-trail מקצה-לקצה,
מוסיף provenance בלוק→מקורות, אוכף ציטוט→קורפוס, ומגלה drift בין DOCX-החי לבלוקים.

| GAP | בעיה (מאומת בקוד) | יעד FU-7 |
|-----|--------------------|----------|
| GAP-18 | `log_action` נכתב רק ב-`case_subtype_override` (cases.py:203) | קריאות `log_action` ב-4 פעולות משנות-מצב: upload, extract_claims, write_block, export |
| GAP-19 | `decision_blocks` נושא `model_used` בלבד — אין קישור לקטעי-מקור | רשומת provenance ב-`audit_log.details` עם source ids שהזינו את הגנרציה |
| GAP-20 | אין אכיפה שציטוט פתיר לקורפוס | ולידציה דטרמיניסטית של `case_law_id` בציטוטים → flag לבלתי-פתירים |
| GAP-17 | `active_draft_path` הופך SoT אחרי revise/apply בלי re-sync לבלוקים | דגל `blocks_stale` דטרמיניסטי + חשיפת drift ב-health-check (לא re-sync שביר) |

## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)

| החלטה | נימוק | מקורות |
|-------|--------|--------|
| provenance כ-**event ב-`audit_log` append-only** (details payload), לא עמודה/טבלה חדשה | דפוס lineage בוגר: entity-key + event-type + actor + source-ids; X5 §4 (סימטריה) | Snowflake data-lineage; OvalEdge provenance; DesignGurus append-only audit |
| GAP-17 = **detect + flag**, מקור-אמת=בלוקים, לא auto-resync | auto-remediation דורש rollback אמין; reparse DOCX→blocks שביר (edits שוברים מבנה) | Flux GitOps drift; Terraform drift (env0); Spacelift |
| GAP-20 = **ולידציה מבנית** של `case_law_id` פתיר, לא NLP של ציטוט חופשי | NLP-ציטוט עברי חופף ל-`extract_internal_citations` הקיים; INV-AUD3 מנוסח סביב פתירוּת `case_law_id` | X5 INV-AUD3; RAG attribution (Lewis 2020); ISO 8000 |
| audit כ-**non-fatal** (כשל-audit מתעד warning, לא מפיל פעולה) | git הוא שכבת-השלמות (X5 §2.1); audit_log הוא observability "מי/מה/מתי" | X5 §2.1; דפוס audit fire-safe |

## 3. הקבצים

- **Modify** `tools/audit.py` — אין שינוי לחתימת `log_action`; להוסיף helper `log_action_safe(...)` שעוטף ב-try/except (warning, non-fatal) כדי שכשל-audit לא יפיל את הפעולה.
- **Modify** `tools/documents.py` — `document_upload` (~:14) + `extract_claims` (~:300): קריאת `log_action_safe`.
- **Modify** `services/block_writer.py` — `write_block`/`store_block` (~:1010): לאסוף source ids מ-context builders + לכתוב audit `write_block` עם provenance.
- **Modify** `tools/drafting.py` — `export_docx` (~:384): audit `export_docx`; `revise_draft` (~:647) + `apply_user_edit` (~:569): סימון `blocks_stale=true`.
- **Modify** `services/db.py` — מיגרציה V22: עמודת `cases.blocks_stale boolean DEFAULT false`; helper `mark_blocks_stale(case_id, val)`; helper `resolve_citation_case_law_ids(ids)` (בדיקת קיום); helper `audit_provenance_query` (קריאה — לא חובה).
- **Modify** `services/qa_validator.py` (או היכן שרץ QA) — בדיקת ציטוט→קורפוס: לכל `case_law_id` בציטוטי-הבלוק, אם לא פתיר → ממצא-QA (warning) + audit `citation_unresolved`.
- **Modify** health-check (metrics.py / processing_status) — חשיפת `cases_with_stale_blocks` count.
- **Test** `tests/test_audit_provenance.py` (חדש) — offline, monkeypatched.

**גבול:** אין שינוי לחתימות ציבוריות; אין מיגרציית-נתונים. provenance של בלוקים *קיימים* לא נאכף
רטרואקטיבית (forward-only) — תואם FU-1/FU-2a.

## 4. GAP-18 — audit על כל פעולה משנה-מצב

`log_action_safe(action, case_id=, document_id=, details=, user=)` — עטיפת `log_action` ב-try/except
(כשל → `logger.warning`, ה-action ממשיך). נקודות-הקריאה:

| פעולה | action | details |
|-------|--------|---------|
| document_upload | `"document_upload"` | `{title, doc_type, classification}` |
| extract_claims | `"extract_claims"` | `{docs_processed, claims_count}` |
| write_block (GAP-19) | `"write_block"` | `{decision_id, block_id, model_used, generation_type, source_document_ids, retrieved_case_law_ids, claim_ids}` |
| export_docx | `"export_docx"` | `{path, file_size, block_count}` |

## 5. GAP-19 — provenance בלוק→מקורות

`write_block` כבר אוסף הקשר מ-`_build_source_context` (document chunks), `_build_precedents_context`
(`para_results`/`caselaw_rows` → `case_law_id`s), `_build_claims_context` (claim ids). היעד: לאסוף את
המזהים הללו ל-dict `sources = {document_ids, case_law_ids, claim_ids}` ולכלול אותו ברשומת ה-audit
`write_block` (§4). כך `audit_log` עונה "מאיזו פסיקה/מסמך נולד הבלוק" — בלי עמודה/טבלה חדשה.
מפתח-הקישור: `details.decision_id`+`details.block_id` (audit_log עצמו keyed ב-case_id/document_id).

## 6. GAP-20 — ציטוט→קורפוס נאכף

`resolve_citation_case_law_ids(ids) -> {resolved: [...], unresolved: [...]}` — בדיקת `EXISTS` מול
`case_law`. בנקודת ה-QA (לפני export, משתלב עם שערי FU-6): לאסוף את כל ה-`case_law_id` מציטוטי-הבלוקים
(`decision_paragraphs.citations` אם מאוכלס, אחרת מ-provenance של §5), ולהריץ resolve. בלתי-פתירים →
**ממצא-QA (warning, לא חוסם-קריטי)** + audit `citation_unresolved`. אכיפה מבנית בלבד (case_law_id),
לא חילוץ-NLP של ציטוט חופשי.

> **הערה:** `decision_paragraphs` אינו מאוכלס כיום ע"י אף כלי (ממצא Explore). לכן ולידציית-הציטוט
> פועלת על ה-`case_law_id`s שנרשמו ב-provenance (§5); אם/כאשר decision_paragraphs יאוכלס — אותה
> ולידציה חלה עליו. זה שומר את ה-GAP סגור בלי לבנות צינור-ציטוטים חדש (מחוץ-להיקף).

## 7. GAP-17 — drift בין DOCX-חי לבלוקים

מקור-אמת = `decision_blocks` (INV-EX1). אחרי `revise_draft`/`apply_user_edit` שהופכים את
`active_draft_path` ל-SoT-בפועל בלי re-sync, מסמנים `cases.blocks_stale=true` (חוזה מפורש: "הבלוקים
ידועים כלא-מסונכרנים מול ה-DOCX-החי"). `export_docx` מ-blocks מאפס `blocks_stale=false` (הבלוקים שוב SoT).
health-check חושף `cases_with_stale_blocks`. **לא** מבצעים reparse DOCX→blocks (שביר).

| נקודה | פעולה על blocks_stale |
|-------|------------------------|
| revise_draft / apply_user_edit | `= true` (DOCX-חי חרג מהבלוקים) |
| export_docx (מ-blocks) | `= false` (בלוקים = SoT שוב) |
| write_block / save_block_content | `= false` (בלוק עודכן ב-DB) |

## 8. שינויי-התנהגות וסיכון

| שינוי | השפעה | סיכון |
|--------|--------|--------|
| audit על 4 פעולות | audit_log מתמלא; observability | נמוך — non-fatal, לא משנה תוצאת-פעולה |
| provenance ב-write_block audit | רשומת מקור לכל גנרציה חדשה | נמוך — forward-only; בלוקים קיימים לא מושפעים |
| ציטוט-QA warning | ציטוט בלתי-פתיר מסומן לאימות-יו"ר | נמוך — warning, לא חוסם export (לא קריטי) |
| `blocks_stale` flag | חשיפת drift; אינו חוסם | נמוך — דגל אינפורמטיבי; V22 additive |

## 9. אסטרטגיית בדיקה

`tests/test_audit_provenance.py` — offline, monkeypatch DB pool. מקרים:
1. `log_action_safe` בולע כשל-DB (warning) ולא מרים.
2. כל אחת מ-4 הפעולות קוראת ל-audit עם ה-action הנכון (monkeypatch log_action, assert call).
3. write_block audit כולל `source_document_ids`/`retrieved_case_law_ids` מה-context.
4. `resolve_citation_case_law_ids`: מפריד resolved/unresolved נכון (monkeypatch EXISTS).
5. ציטוט בלתי-פתיר → ממצא-QA warning (לא חוסם-קריטי).
6. `blocks_stale`: revise/apply → true; export-from-blocks → false.
7. health-check חושף `cases_with_stale_blocks`.

> בדיקות-DB אמיתיות (audit_log INSERT, V22, EXISTS) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a.

## 10. סדר-ביצוע

1. בדיקות אדומות.
2. `log_action_safe` + מיגרציה V22 (`blocks_stale`) + helpers (`mark_blocks_stale`, `resolve_citation_case_law_ids`).
3. GAP-18: 4 קריאות audit (upload, extract_claims, export_docx + write_block בסיס).
4. GAP-19: איסוף source ids ב-write_block → provenance ב-audit.
5. GAP-20: ולידציית-ציטוט ב-QA + audit `citation_unresolved`.
6. GAP-17: `blocks_stale` ב-revise/apply/export/write_block + health-check.
7. בדיקות ירוקות + smoke מול DB + lint + TaskMaster.
