Files
legal-ai/docs/case-deletion-runbook.md
Chaim cd4eed0045
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
docs: case-deletion runbook (legal-ai + Paperclip + Gitea)
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>
2026-04-30 14:54:21 +00:00

8.6 KiB
Raw Blame History

מחיקת תיק — 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.


תהליך מחיקה מלא — שלב אחרי שלב

הצב את מספר התיק במשתנה לפני שמתחילים:

CASE_NUMBER=8174-24
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 ה-endpoint הזה החזיר 500 כי db.delete_case לא היה מוגדר. אם נתקלת ב-500 בגרסה ישנה, השתמש ב-SQL הישיר (ראה Fallback בסוף).

שלב 2 — Paperclip

אין API. SQL ישיר:

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 נוצר)

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 — וידוא ניקיון

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 יבצעו את העבודה:

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:

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 אינטראקטיבי. אסור שיעבוד בלי אישור מפורש.