Fixed in https://github.com/badbundle/vault-app/pull/635 (merged as 1455524a).
The change: Import & Override is now all or nothing. PersistedLocalVaultStore.importAndOverrideVault stages the whole replacement in a separate ModelContext on the same container, then saves once, in one SQLite transaction. If an item doesn't encode or the save fails, the staged context is discarded, so the vault is exactly as it was in memory and on disk.
Why not one save plus rollback(): that was the first approach, and it crashes inside SwiftData's rollback() once a context has deleted the old items and tags and then inserted or updated others. A discarded context is never rolled back.
Details:
- Items the backup shares with the vault are updated in place, and the rest are deleted or inserted. That avoids SwiftData's unique-id upsert merging old tag relationships.
- #627's scrub still runs after a successful import.
RecordVaultStore, and so the encrypted store, already built the new state before swapping it in.
Tests:
- Contract suite on all three engines: a failed override keeps the same items, tags and order; an import succeeds after a failed one; an override replaces loaded items; merge failure is pinned.
- A SQLite trigger that fails the save part way leaves the vault intact, before and after reopening.
- A residue test for items overwritten in place.
- A mutation check: putting back the old import fails both fault-injection tests.