Skip to content

Lau 20 document deletion storage fix - #337

Open
t1ffnyw wants to merge 20 commits into
mainfrom
lau-20-document-deletion-storage-fix
Open

Lau 20 document deletion storage fix#337
t1ffnyw wants to merge 20 commits into
mainfrom
lau-20-document-deletion-storage-fix

Conversation

@t1ffnyw

@t1ffnyw t1ffnyw commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Closes the storage deletion P0: document/version deletes no longer claim success on SQL alone; they write a durable deletion plan, delete provider objects asynchronously, then purge relational rows only after storage is confirmed clean.
  • Freezes the ObjectRef / DeleteResult / StoragePort contract and dual-writes a storage_objects manifest (+ artifact lineage) on successful uploads and derived writers (ZIP, audio/video transcripts, DOCX edits, versions).
  • Adds deletion status APIs + UI polling (never treat non-completed as success), metrics/flag dashboard, orphan-audit tooling (read-only by default), and staged rollout docs/flags.

Related

Issue #313
Lau 20

Checklist

  • pnpm check passes (lint + typecheck)
  • pnpm --filter @launchstack/web test passes
  • Changeset added (if packages/core/ changed — pnpm changeset)
  • New env vars documented in .env.example and apps/web/src/env.ts
  • UI changes exercised in a browser (not just a green build)
  • Docs updated if behavior changed

Testing

Notes for reviewers

  • Rollout is flag-gated and off by default. Worker flag off first on rollback, then lifecycle flag. Relational purge still waits for storage-clean confirmation.
  • Do not invent a third env flag. URL→ref parsing stays in promoteLegacyUrlToRef / adapters only.
  • C3 orphan audit does not delete. --backfill only registers high-confidence manifested refs; confirmed-orphan cleanup needs a separate approved run.
  • Risky areas: ZIP source cleanup gated by lifecycle flag; DOCX modifyDocument registers new object + supersedes edge; dual-write ownership moves from document → version on initial version create.
  • Pre-existing main Vercel redness may also fail this PR preview for the same env/config reasons — call out if failures match main rather than this branch.

@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
launch-stack Error Error Aug 18, 2026 8:00pm
pdr-ai-v2 Error Error Aug 18, 2026 8:00pm

Merge e84b7fa was committed with conflict markers still in the files and pushed to origin. packages/core/src/storage/index.ts had markers inside an export statement - a syntax error - so nothing importing @launchstack/core/storage would load, and the whole deletion lifecycle plus every test script was unbuildable.

Also fixes damage git merged silently, with no markers to spot it by: ObjectRef/DeleteResult declared twice in types.ts, deleteRef/deleteMany defined twice in port.ts, duplicate ref keys in uploadthing/core.ts and upload-local/route.ts, and a duplicate ObjectRef import in AddSourceModal.tsx.

Upload path standardised on storageRef (not documentRef): processDocumentUpload already takes storageRef, and uploadDocument/route.ts already called resolveUploadRef outside the conflicted region. HEAD's canonical-ref check (Decision 5) is preserved, rewritten against the storageRef naming.

legacy-promote.ts takes the HEAD side because the other one references an undeclared dbPathMatch and does not compile.

The version-delete route is resolved to A6b (the coordinator cutover) and marked PROVISIONAL in a comment - design doc A6b says to swap this route onto requestVersionDeletion once B1 shipped and delete the A6a path, and the code below the conflict already assumes the A6b result shape. Dev A owns A6; confirm before this is pushed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01X5Ag5CzaNtNtTrq2Hvp4qQ
…+ file tenant auth

B3 item 2: the worker no longer switches per adapter and deletes one key at a time. Items are grouped by ref identity and go out through Dev A's deleteManyByRef, which batches by (adapter, storageLocationId) - one S3 DeleteObjects call per group, per-ref outcomes so a partial batch retries only what failed (A7).

Fixes a real bug that grouping exposed: s3, vercel-blob and uploadthing were called with the bare key, so the Decision 4 stale-location guard never ran for three of four adapters. A ref minted against a retired bucket or Blob store would have been deleted out of whatever store is configured now. It is reported blocked instead.

B3 item 3: rejected mapped to blocked, contradicting Decision 2 and the shipped mapDeleteOutcomeToItemState. Removed the local duplicate; the frozen mapping is now the only one, so rejected reaches QUARANTINED and dominates manual_review.

B3 item 10: adds scripts/test-storage-failure-matrix.ts - transient retry, budget exhaustion, stale location, unknown adapter, missing-object idempotency, partial batch, quarantine dominance, plus B8. Updates the B3 worker test, which was silently relying on the database adapter being unimplemented to produce BLOCKED items; that stopped being true when A3 shipped. It now blocks deliberately via a stale location, and Part D runs the real delete instead of faking one.

B8: adds api-response-schemas.ts (zod shapes for B7's status payload and the B4/B5 bodies, validated outside production only and never able to throw), and file-ownership.ts, which derives a company for a file_uploads row - that table records only an uploader, never a tenant - from manifest, document, version, then uploader, reporting which source answered since they are not equal evidence.

Wires that into /api/files/[id], which had no authentication at all and is enumerable. Defaults to log-only via STORAGE_FILE_TENANT_AUTH_MODE: it reports what it would refuse and refuses nothing, because the ingestion path fetches these files server-to-server with no session and enforcing blind would surface as failed document processing.

Collapses three flag readers into one. Adds both deletion flags plus the new mode to .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01X5Ag5CzaNtNtTrq2Hvp4qQ
… 0022

0022 drops the ON DELETE SET NULL foreign key on storage_deletion_tombstones.request_id, because storage_deletion_requests.document_id is ON DELETE CASCADE - completing a deletion destroys the request row, and the SET NULL then wipes the tombstone's only pointer back to it, exactly when someone holding a request id wants to ask what happened (B7 finding 2).

base.ts still declared that FK. db:push regenerates the schema from base.ts, so any database built the documented dev way got the constraint back and the fix silently reverted. Caught on a real run: the worker inserts requestId one line before the purge, and the tombstone came back with requestId null, making getDeletionStatusByRequestId's tombstone fallback permanently unmatchable.

Part K of the failure-matrix script (added in the previous commit) purges a document and asserts both that the tombstone keeps its request id and that status-by-request-id still answers, so this cannot regress silently again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants