Files
legal-ai/docs/spec/X14-storage-minio.md
Chaim 16470f6279 docs(spec): X14 — object-storage (MinIO/S3) migration plan
Adds docs/spec/X14-storage-minio.md — the domain spec + phased plan for
migrating binary document storage from the local data/ tree to the
already-deployed MinIO service (Coolify svc `minio`).

Captures: disk inventory, scattered file-I/O map (~8 services, no central
layer), DB path columns, MinIO deploy state, Paperclip = API-consumer only.
Defines 7 domain invariants (INV-STG1..7) and a 7-phase execution plan.

Chair decisions (2026-06-08): git-per-case keeps text/metadata + MinIO holds
binaries (INV-STG7); WORM Object-Lock on FINAL decisions only (INV-STG4);
internal Docker network for legal-ai↔MinIO.

Invariants: keeps G2 (single storage path replaces scattered I/O);
INV-STG1..7 new. Spec-only PR — no code/behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 06:30:06 +00:00

147 lines
11 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.
# X14 — אחסון-אובייקטים (Object Storage: MinIO / S3)
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **אחסון קבצים בינאריים**
מסמכי-מקור, נגזרים, וייצוא — והגירתם ממערכת-קבצים מקומית (`data/`) ל-**MinIO** (object store תואם-S3).
הוא מגדיר את חוזה-האחסון (שכבה יחידה), סכמת-הדליות-והמפתחות, מודל-האי-שינויוּת המשפטי, ותוכנית-ההגירה.
> **invariant הנדסי + תפעולי-משפטי.** INV-STG1/2/5/6 נשענים על עקרונות מוכרים (S3 API, 12-Factor, presigned-URL,
> separation blob↔metadata) — ≥3 מקורות (docs.min.io, AWS S3 spec, minio-py). INV-STG3/4/7 הם תפעוליים/משפטיים
> של *מערכת זו* (גבול-ממשל, WORM להחלטות חתומות, git=טקסט) ונקשרים ל-[G2](00-constitution.md) (מסלול-אחסון יחיד).
---
## 1. מצב קיים (מאומת מול הקוד וה-infra, 2026-06-08)
### 1.1 מלאי-הדיסק (`data/`, ללא `backups/`)
| קטגוריה | נפח | תוכן | סוג |
|---|---|---|---|
| `data/cases/{case}/` | 1.2GB | `documents/{originals,extracted,proofread,research,backup}`, `drafts/`, `exports/`, `thumbnails/{doc_uuid}/pNNN.jpg`, `.git` per-case | מקור + נגזר |
| `data/digests/{reference,incoming}/` | 251MB | יומונים (X12) | מקור |
| `data/training/{cmp,cmpa}/{raw,proofread}/` | 157MB | קורפוס-קול + `.git` | מקור |
| `data/precedent-library/{appeals_committee,court_ruling,other}/` | 105MB | פסיקה + `thumbnails/` | מקור |
| `data/internal-decisions/{region}/` | 45MB | החלטות-פנים לפי מחוז | מקור |
| `data/exports/` | 216KB | legacy (הוחלף ב-per-case) | נגזר |
| `data/{audit,eval,logs}/` | ~52MB | CSV/JSON תפעוליים — **לא מסמכים, נשארים בדיסק** | תפעולי |
ספירה (ללא backups): ~9,449 קבצים — 2,473 JPG (thumbnails נגזרים), 883 PDF, 250 TXT (extracted), 155 DOCX, 54 DOC.
### 1.2 הקונטיינר (Coolify)
legal-ai (`gyjo0mtw2c42ej3xxvbz8zio`) רץ עם **bind-mounts**: host `data/``/data`, host `data/cases/``/cases`.
האחסון היום = תיקייה על המארח, חשופה ישירות.
### 1.3 MinIO — **כבר פרוס ובריא** ✅ (שירות Coolify `minio`, `bx2ykvw94xbutsex41hz4vv8`, 2026-06-08)
- **API:** `https://s3.nautilus.marcusgroup.org` (9000) · **Console:** `https://minio.nautilus.marcusgroup.org` (9001)
- **Credentials:** `SERVICE_USER_MINIO` / `SERVICE_PASSWORD_MINIO` (סודות מנוהלי-Coolify)
- **אחסון:** named-volume `minio-data``/data`**Single-Node Single-Drive**; versioning/object-lock **לא** מופעלים עדיין
- **רשת:** רשת-Docker משלו (`bx2ykvw...`, external), **לא** משותפת ל-legal-ai → דרושה קישוריות (§4 שלב 0)
### 1.4 הקוד — **אין שכבת-אחסון מרכזית** (כשל-השורש שהתחום מייבש)
ה-I/O מפוזר על ~8 שירותים, נתיבים נבנים inline:
- העלאה: `tools/documents.py:54` (originals), `:152` (training)
- חילוץ + thumbnails: `services/processor.py:43,153`
- staging פסיקה/יומונים/החלטות: `services/ingest.py:69`
- ייצוא DOCX: `services/docx_exporter.py:462`
- הגשה (FileResponse): `web/app.py` — 6 endpoints
- git per-case: `services/git_sync.py` (`git add .` + push ל-Gitea, sweep כל 30ש׳)
### 1.5 עמודות-DB המאחסנות נתיבים (schema inline ב-`db.py`, ללא migrations)
`documents.file_path` · `cases.active_draft_path` · `case_law.source_document_path` · `digests.source_document_path`
· `document_image_pages.image_thumbnail_path` · `precedent_image_pages.image_thumbnail_path` · `draft_final_pairs.final_path`
### 1.6 Paperclip — צרכן-API בלבד
הפלאגין ניגש דרך `listDocuments`/`getDocumentText` ל-API (`plugin-legal-ai/src/legal-api.ts:89`). אינו נוגע בדיסק →
**הגירה שקופה אליו** כל עוד ה-API יציב.
---
## 2. Invariants של התחום
### INV-STG1: שכבת-אחסון יחידה — כל I/O דרך `storage.py`
**כלל:** קיים מודול-אחסון **יחיד** (`services/storage.py`) שכל קריאה/כתיבה של קובץ בינארי עוברת דרכו
(`put/get/presign_get/presign_put/delete/list`). אסור `open()`/`shutil.copy()`/`Path.write_bytes()` ישיר על
נתיב-אחסון מחוץ למודול. **מקיים [G2](00-constitution.md)** — מבטל את ה-I/O המפוזר (§1.4) שהוא מסלול-מקביל-מתפצל.
### INV-STG2: מפתח-אובייקט אטומי; שם עברי במטא בלבד
**כלל:** מפתח-האובייקט הוא ASCII/UUID (`cases/{case}/originals/{uuid}.pdf`). שם-הקובץ העברי המקורי נשמר ב-DB
(`*_filename`) וכ-`x-amz-meta-filename` + מוגש דרך `Content-Disposition` ב-presigned-GET. **למה:** תקציב-מפתח
1024 bytes (255/segment), עברית=2B/תו, ובעיות percent-encoding/XML — נמנעות.
### INV-STG3: דליות לפי גבול-ממשל, prefix לפי קטגוריה/תיק
**כלל:** versioning/object-lock/replication הם per-bucket → מה שדורש ממשל שונה יושב בדלי נפרד. שלוש דליות
קבועות (§3.1); תיקים/קטגוריות הם prefixes, **לא** דלי-לכל-תיק.
### INV-STG4: "סופי" = WORM (Object-Lock COMPLIANCE)
**כלל:** החלטה חתומה/סופית נכתבת לדלי `legal-immutable` עם Object-Lock **COMPLIANCE** + versioning — בלתי-ניתנת
לשינוי/מחיקה ע"י איש (כולל root) עד תום-תקופת-השמירה. טיוטות חיות בדלי רגיל ו"מקודמות" (copy) לדלי-הסגור עם החתימה.
**(הכרעת-יו"ר 2026-06-08: סופי בלבד; מסמכי-מקור — versioning ללא נעילה קשיחה.)**
### INV-STG5: pgvector נשאר מקור-האמת לטקסט/embeddings; MinIO = blob בלבד
**כלל:** טקסט-מחולץ + embeddings נשארים ב-Postgres/pgvector (מקור-אמת לאחזור). MinIO מאחסן את ה-blob המקורי
(+עותק-ארכיון אופציונלי של ה-extracted text). **אסור** ש-MinIO יהיה מקור-אמת לוקטורים. תואם
`no-reocr-retrofit` — לא מריצים OCR מחדש בהגירה.
### INV-STG6: הגשה לדפדפן דרך presigned-URL — bytes לא דרך FastAPI
**כלל:** הורדה/תצוגה/העלאה מהדפדפן עוברות ב-presigned-URL (TTL דקות) מול `s3.nautilus.marcusgroup.org`.
ה-backend מנפיק את ה-URL בלבד; ה-bytes לא עוברים דרכו. endpoints קיימים שמחזירים FileResponse → 302→presigned.
### INV-STG7: git-per-case שומר טקסט/מטא בלבד; בינאריים ב-MinIO
**כלל:** `.git` per-case ממשיך לגרסן `case.json`/`notes.md`/`documents/extracted/*.txt`/`research/*.md`. PDF/DOCX/JPG
מוחרגים מ-tracking (`.gitignore` per-case) ויושבים ב-MinIO. **(הכרעת-יו"ר 2026-06-08.)** `git_sync.py` ו-sweep
מסתמכים על אותו working-tree → ההחרגה חייבת לקדום לכל קומיט-הגירה כדי לא לשבור היסטוריה.
---
## 3. ארכיטקטורת-היעד
### 3.1 דליות ומפתחות
| דלי | Versioning | Object-Lock | prefixes |
|---|---|---|---|
| `legal-documents` | ✅ | ❌ | `cases/{case}/originals/{uuid}.pdf` · `cases/{case}/proofread/{uuid}.txt` · `precedent-library/{type}/{uuid}.pdf` · `internal-decisions/{region}/{uuid}.pdf` · `digests/{uuid}.pdf` · `training/{cmp\|cmpa}/{raw\|proofread}/{uuid}.pdf` |
| `legal-immutable` | ✅ | ✅ COMPLIANCE | `decisions-final/{case}/{uuid}.docx` (החלטות חתומות בלבד) |
| `legal-derived` | ❌ | ❌ (+lifecycle) | `thumbnails/{doc_uuid}/pNNN.jpg` · `extracted/{uuid}.txt` (נגזר, ניתן-לשחזור) |
### 3.2 `services/storage.py` (לב ההגירה) — adapter כפול
```
put(category, key, data, content_type, meta) -> uri # category→bucket+prefix
get(uri) -> bytes
presign_get(key, ttl) / presign_put(key, ttl) -> url
delete(key) / list(prefix)
```
backend נבחר ב-env `STORAGE_BACKEND ∈ {filesystem, dual, s3}` (ברירת-מחדל filesystem) — מאפשר מעבר הדרגתי ללא
שינוי-התנהגות. SDK: `aioboto3` (async-native מול `endpoint_url=http://minio:9000`); `minio-py` לסקריפטי-הגירה.
### 3.3 שינויי-DB
הוספת `*_object_key` (או נרמול ל-`storage_uri` עם סכמה `s3://`/`file://`) לצד העמודות הקיימות (§1.5); backfill;
דה-קומיישן הנתיב-קובץ. תוספת inline ב-`db.py` בסגנון הקיים (אין migrations).
---
## 4. תוכנית-ביצוע בשלבים (→ TaskMaster, tag legal-ai)
| שלב | תוכן | תלות |
|---|---|---|
| **0 — תשתית** | חיבור רשת-Docker (minio↔legal-ai); הזרקת credentials ל-env legal-ai (Coolify); `mc alias`; יצירת 3 דליות + הפעלת versioning + Object-Lock (immutable); הוספת `aioboto3` ל-deps | — |
| **1 — שכבת-אחסון** | `services/storage.py` + adapter כפול (default filesystem). אפס שינוי-התנהגות. PR מצהיר INV-STG1/2/3 | 0 |
| **2 — חיווט-כתיבה** | הפניית כל נקודות-הכתיבה (§1.4) דרך `storage.py`; כתיבה-כפולה (`STORAGE_BACKEND=dual`) | 1 |
| **3 — הגירת-נתונים** | `mc mirror --dry-run``--overwrite` של 5 הקטגוריות; backfill `*_object_key` ב-DB; אימות count+checksum | 0,2 |
| **4 — חיווט-קריאה + presigned** | endpoints→302→presigned; thumbnails דרך presigned; dual-read (S3, fallback disk); החרגת בינאריים מ-git per-case (INV-STG7) | 2,3 |
| **5 — cutover** | `STORAGE_BACKEND=s3`; `mc mirror --watch` עד החלפה; אימות מלא; כיבוי כתיבה-לדיסק | 4 |
| **6 — git + גיבוי + ניקוי** | קידום-החלטות-סופיות ל-immutable (INV-STG4); `mc mirror`/bucket-replication מתוזמן off-site; דה-קומיישן bind-mount `data/` (השארת audit/eval/logs) | 5 |
---
## 5. סיכונים
- **I/O מפוזר** → INV-STG1 (`storage.py`) חובה לפני כל שאר השלבים, אחרת drift והפרת-G2.
- **שמות עבריים כמפתחות** → INV-STG2 (UUID-keys + מטא).
- **רשת נפרדת ל-MinIO** → לאמת קישוריות בשלב 0 לפני הכל.
- **git-per-case** מצמיד בינאריים ל-Gitea → INV-STG7, ההחרגה חייבת לקדום לכל קומיט.
- **SNSD ללא erasure-coding** → גיבוי off-site (שלב 6) הוא חובה, לא nice-to-have.
- **בידוד-worktree + ספ-first** → כל PR מצהיר invariants (G2 + INV-STG*).
---
## 6. קישורים
- חוקה: [00-constitution.md](00-constitution.md) · נתונים: [02-data-model.md](02-data-model.md) · קליטה: [01-ingest.md](01-ingest.md)
- deploy/env: [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) · אינטגרציה: [X3-integration-deploy.md](X3-integration-deploy.md)
- מקורות-MinIO: docs.min.io (community), AWS S3 object-keys/bucket-naming/presigned-URL, github.com/minio/minio-py