docs: case-deletion runbook (legal-ai + Paperclip + Gitea)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s

Captures the full deletion procedure we worked out empirically while
wiping case 8174-24 for a clean rerun. Covers all four systems where
case state lives, in dependency order:

  1. legal-ai DB + on-disk dir — DELETE /api/cases?remove_files=true
     (now actually works after 903fb4d added the missing db.delete_case)
  2. Paperclip DB — no API; raw SQL with explicit FK-blocker ordering
     (issue_comments, cost_events, finance_events, feedback_votes,
     issue_inbox_archives, issue_read_states must go before issues;
     heartbeat_runs.wakeup_request_id must be NULLed before
     agent_wakeup_requests can be deleted)
  3. Gitea — DELETE /api/v1/repos/cases/{N}
  4. Verification queries for each system

Two gotchas worth highlighting in the doc:
  • The case directory inside /data/cases is owned by root because the
    container runs as root — host-side rm needs sudo, or use the API
    (rmtree happens inside the container).
  • Paperclip projects are referenced via name LIKE '%{N}%' since
    there's no slug column. Stricter matching is recommended if N
    appears in multiple project names.

Linked from legal-ai/CLAUDE.md docs index. A future scripts/delete-case.sh
that automates the runbook with a confirmation prompt is noted as TODO
inside the runbook itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 14:54:21 +00:00
parent 903fb4d140
commit cd4eed0045
2 changed files with 180 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# מחיקת תיק — runbook
> **מתי להשתמש:** reset שלם של תיק (לבדיקות end-to-end), מחיקת תיק שנפתח בטעות, או ניקיון לפני העלאה חוזרת של מסמכים.
>
> **חשוב:** ה-API `DELETE /api/cases` בלבד **לא מספיק** — הוא מטפל רק בצד legal-ai (DB + on-disk dir). תיק חי במקביל ב-4 מערכות והכול חייב להתנקות יחד.
---
## איפה ה-state של תיק חי
| מערכת | מה נשמר | איך מנקים |
|---|---|---|
| **legal-ai DB** (port 5433) | `cases` + `documents` + `document_chunks` + `claims` + `appraiser_facts` + `decisions` + `qa_results` + `case_precedents` | API DELETE (cascade על FK) |
| **legal-ai disk** | `/data/cases/{N}/` בתוך ה-container — מכיל drafts/, documents/, .git/ | API עם `remove_files=true` (`shutil.rmtree` בתוך ה-container) |
| **Paperclip DB** (port 54329) | `projects` + `issues` + `issue_comments` + `agent_wakeup_requests` + `heartbeat_runs` (audit) + עוד 6+ טבלאות | SQL ידני (אין API) |
| **Gitea** | repo `cases/{N}` אם נוצר ב-case-create | Gitea API |
ה-API לא מטפל ב-Paperclip ו-Gitea כי אלה מערכות חיצוניות שלגמרי מחוץ ל-DB של legal-ai. תועד מפורשות ב-docstring של [`services/db.py:delete_case`](../mcp-server/src/legal_mcp/services/db.py).
---
## תהליך מחיקה מלא — שלב אחרי שלב
הצב את מספר התיק במשתנה לפני שמתחילים:
```bash
CASE_NUMBER=8174-24
```
### שלב 1 — legal-ai (DB + disk)
```bash
curl -s -X DELETE \
"https://legal-ai.nautilus.marcusgroup.org/api/cases?case_number=${CASE_NUMBER}&remove_files=true" \
-w "\nhttp=%{http_code}\n"
```
תוצאה צפויה: `200` עם `{"deleted": true, "removed_files": true, ...}`.
מה זה עושה מאחורי הקלעים:
1. `DELETE FROM cases` — מפעיל **CASCADE** ל-7 טבלאות, **SET NULL** ל-`audit_log` ו-`chair_feedback`.
2. `shutil.rmtree(/data/cases/{N})` — מסיר את כל הספרייה כולל `.git`.
> **הערה:** עד לפני [commit `903fb4d`](https://gitea.nautilus.marcusgroup.org/ezer-mishpati/legal-ai/commit/903fb4d) ה-endpoint הזה החזיר 500 כי `db.delete_case` לא היה מוגדר. אם נתקלת ב-500 בגרסה ישנה, השתמש ב-SQL הישיר (ראה Fallback בסוף).
### שלב 2 — Paperclip
אין API. SQL ישיר:
```bash
PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip <<SQL
BEGIN;
-- 1. מצא את כל ה-issues של הפרויקט (לפי שם)
CREATE TEMP TABLE _issue_ids AS
SELECT i.id, i.identifier
FROM issues i
JOIN projects p ON i.project_id = p.id
WHERE p.name LIKE '%${CASE_NUMBER}%';
SELECT identifier FROM _issue_ids ORDER BY identifier; -- וידוא לפני המחיקה
-- 2. מחק blockers ל-FK עם NO ACTION (אסור למחוק issue אם יש להם reference)
DELETE FROM issue_comments WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM cost_events WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM finance_events WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM feedback_votes WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM issue_inbox_archives WHERE issue_id IN (SELECT id FROM _issue_ids);
DELETE FROM issue_read_states WHERE issue_id IN (SELECT id FROM _issue_ids);
-- 3. מחק את ה-issues. CASCADE מטפל ב-7 טבלאות נוספות:
-- issue_approvals, issue_attachments, issue_documents,
-- issue_execution_decisions, issue_labels, issue_relations,
-- issue_work_products
DELETE FROM issues WHERE id IN (SELECT id FROM _issue_ids);
-- 4. שבור FK מ-heartbeat_runs כדי שאפשר יהיה למחוק wakeup_requests.
-- heartbeat_runs נשמרים כ-audit log לא משויך.
UPDATE heartbeat_runs
SET wakeup_request_id = NULL
WHERE wakeup_request_id IN (
SELECT id FROM agent_wakeup_requests
WHERE payload->>'issueId' IN (SELECT id::text FROM _issue_ids)
);
DELETE FROM agent_wakeup_requests
WHERE payload->>'issueId' IN (SELECT id::text FROM _issue_ids);
-- 5. מחק blockers ברמת ה-project (NO ACTION FK ל-projects)
DELETE FROM cost_events WHERE project_id IN (SELECT id FROM projects WHERE name LIKE '%${CASE_NUMBER}%');
DELETE FROM finance_events WHERE project_id IN (SELECT id FROM projects WHERE name LIKE '%${CASE_NUMBER}%');
-- 6. מחק את הפרויקט. CASCADE מטפל ב:
-- execution_workspaces, project_goals, project_workspaces, routines
DELETE FROM projects WHERE name LIKE '%${CASE_NUMBER}%' RETURNING id, name;
COMMIT;
SQL
```
> **למה Paperclip לא הוסיף API למחיקה?** כי זאת מערכת רב-משתמשית ומחיקה היא הרסנית מטבעה — Paperclip מעדיף `archive` (`projects.archived_at`). אנחנו אכן רוצים מחיקה אמיתית רק לסביבת בדיקות.
### שלב 3 — Gitea (אם repo נוצר)
```bash
GITEA_TOKEN=$(infisical secrets get GITEA__API_TOKEN --silent || \
echo "$GITEA_TOKEN") # סגדור מ-Infisical או ENV
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.nautilus.marcusgroup.org/api/v1/repos/cases/${CASE_NUMBER}" \
-w "http=%{http_code}\n"
```
תוצאה צפויה: `204` (deleted) או `404` (לא נוצר מעולם).
### שלב 4 — וידוא ניקיון
```bash
echo "=== legal-ai ==="
PGPASSWORD=$LEGAL_AI_PG psql -h localhost -p 5433 -U legal_ai -d legal_ai -t -c "
SELECT count(*) FROM cases WHERE case_number = '${CASE_NUMBER}';
" # → 0
ls /home/chaim/legal-ai/data/cases/${CASE_NUMBER} 2>&1 | head -1
# → "No such file or directory"
echo "=== Paperclip ==="
PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -t -c "
SELECT 'projects:'||count(*) FROM projects WHERE name LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'issues:'||count(*) FROM issues WHERE title LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'comments:'||count(*) FROM issue_comments WHERE body LIKE '%${CASE_NUMBER}%'
UNION ALL SELECT 'wakeups:'||count(*) FROM agent_wakeup_requests WHERE payload::text LIKE '%${CASE_NUMBER}%';
" # → all 0
echo "=== Gitea ==="
curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.nautilus.marcusgroup.org/api/v1/repos/cases/${CASE_NUMBER}" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('full_name','NOT FOUND'))"
# → NOT FOUND
```
---
## Fallback — אם ה-API נשבר
אם משום מה ה-API DELETE לא עובד (ראינו את זה בעבר עם `delete_case` החסר), עשה DELETE ישיר ב-DB. ה-FK constraints יבצעו את העבודה:
```sql
PGPASSWORD=$LEGAL_AI_PG psql -h localhost -p 5433 -U legal_ai -d legal_ai -c "
DELETE FROM cases WHERE case_number = '${CASE_NUMBER}' RETURNING case_number, title;
"
```
לאחר מכן הסר את הספרייה מהדיסק. הספרייה בבעלות `root` כי ה-container רץ כ-root, אז תצטרך `sudo`:
```bash
sudo rm -rf /home/chaim/legal-ai/data/cases/${CASE_NUMBER}
```
---
## הערות שנלמדו תוך כדי
1. **`heartbeat_runs.wakeup_request_id`** הוא ה-trap היחיד. הוא NO ACTION FK, ולכן חוסם מחיקה של `agent_wakeup_requests`. הפתרון: `UPDATE ... SET wakeup_request_id = NULL` לפני המחיקה. ה-runs עצמם נשמרים כ-audit log (לא הפסד).
2. **פרויקט "name" ב-Paperclip** — לפי הקונבנציה הוא מתחיל ב-"ערר {N}" — לכן `LIKE '%{N}%'` מספיק. אם יש מספר תיקים שמכילים את אותו מספר, להחמיר עם match מלא או לפי `id`.
3. **Container ↔ host file ownership** — קבצים שיוצר ה-container (כולל ספריית התיק) שייכים ל-`root`. מחיקה מהמארח דורשת `sudo`, או דרך docker exec, או דרך ה-API (שמבצעת `rmtree` בתוך ה-container).
4. **`audit_log` ו-`chair_feedback` נשארים** — FK שלהם הוא SET NULL כדי לשמור היסטוריה גם אחרי שהתיק נמחק. אם אתה צריך מחיקה היסטרית מוחלטת, מחק שורות אלה ידנית.
---
## TODO — אוטומציה
ה-runbook הזה ניתן להמרה לסקריפט `scripts/delete-case.sh` שמקבל `CASE_NUMBER` ומבצע את 4 השלבים עם prompt confirmation. עדיין לא הוטמע — נכון להיום העבודה ידנית.
מי שמטמיע: שמור את הסקריפט כ-`destructive` ב-SCRIPTS.md ודרוש `--confirm` או prompt אינטראקטיבי. אסור שיעבוד בלי אישור מפורש.