Imported attachment and staging paths left as the case folder by the old import helper#1610

openedSep 27, 2026byMMatt Donovan

Defect: attachments and staged files from some earlier imports still point at the case folder.

What: Before the import fix in the imported-capture-paths change, importing an archive exported on Windows or one that held a bare file name stored the case folder as the file's location. Export then tries to read a folder as a file, and staged files cannot be committed. The capture repair migration only fixes captures. It was left out of that change because those rows lost their file extension, so a repair has to find each file on disk.

Where it is and how to reproduce it

Where: src/main/services/db/migrations.ts:1055 (v37 repairs only kind = 'capture' and never visits staging_files); the old helper is rerootPath in src/main/services/db/exhibitRepo.ts:297, fixed in #1609. Reproduce: On a build before #1609, import a schema-6 archive exported on Windows that holds an attachment Exhibit or a staged file, upgrade, then export the case: the attachment row's path equals the case id.

Found by Codex review on #1609 (discussion_r4116165119).

Note

Claude responding on behalf of Matt.

This was generated by AI during triage.

Agent Brief

Category: bug, evidence-affecting Summary: Repair attachment and staged-file records that an older import left pointing at the Case folder instead of at their file.

Triage confirmed from the code on 2026-10-04 that the existing repair fixes Capture records only. Attachment and staged-file records written by an import before the 27 September fix still name the Case folder as the file's location, so export fails on them and staged files cannot be committed.

The maintainer's ruling: repair them. A record is repaired only when exactly one file on disk matches it. Anything else is left as it is and logged.

Current and desired behavior, interfaces, acceptance criteria, verification and scope

Current behavior (read at origin/main 43906e63, not run)

  • The schema 37 migration re-roots captures paths and copies the repaired path onto exhibits rows of kind = 'capture'. It does not visit other Exhibit kinds or staging_files.
  • Before #1609, rerootPath turned an archive path with Windows separators, or a bare file name, into the new Case id alone. Those rows lost their file name and extension.
  • The import writes each non-Capture Exhibit's file to the Case directory under the subdirectory for its kind (EXHIBIT_KIND_SUBDIRECTORIES), and each staged file under STAGING_SUBDIRECTORY, named <row id><original extension>.

Desired behavior

A new schema migration, scaffolded with pnpm db:migration:new, that for every non-Capture exhibits row and every staging_files row whose path is exactly its Case id:

  • looks in the directory the import would have written to for files named <row id> plus any or no extension;
  • on exactly one match, sets path to the Case-relative path in the form the fixed rerootPath produces today;
  • on zero or several matches, leaves the row untouched and logs the row id and the count, with no absolute path in the message.

Rows whose path already names a file are never touched. The migration is idempotent and never moves, renames or deletes a file. content_hash and every Manifest are unchanged.

Key interfaces

  • migrations.ts: one new fail-closed version block; LATEST_SCHEMA_VERSION bumped by the scaffold.
  • The storage root lookup the migration uses must match how the schema 37 block or its test obtains it; if a migration has no way to reach the storage root today, stop and report that on the issue instead of inventing one.

Acceptance criteria

  • An attachment Exhibit row whose path equals its Case id, with one matching file on disk, has path pointing at that file after migration, and exporting the Case reads it.
  • A staging_files row in the same state is repaired the same way.
  • A row with no matching file is unchanged and logged.
  • A row with two matching files is unchanged and logged.
  • A row whose path already names a file is unchanged, including a Capture Exhibit row.
  • Running the migration body twice yields the same rows as running it once.
  • No file under the storage root is created, modified or removed by the migration.
  • A database with no affected rows migrates with no row changes.

Verification

pnpm test tests/main/services/importedCapturePathsMigration.test.ts as the model for the new migration test, then pnpm preflight on Node 20.

Starting files

  • src/main/services/db/migrations.ts (the schema 37 block)
  • src/main/services/db/exhibitRepo.ts (rerootPath) and src/main/services/db/stagingRepo.ts
  • src/main/services/caseArchive.ts (where the import writes Exhibit and staged files)
  • tests/main/services/importedCapturePathsMigration.test.ts

Evidence-affecting: yes. The label has been on the issue since it was filed. The migration is on the blocking tier, so the PR carries the label and gets human review.

Out of scope

  • Changing rerootPath or the import, both fixed in #1609.
  • Repairing rows by hashing files to find a match.
  • Any UI for rows the migration could not repair.