# Instance model — design plan (2026-06-16)

## Decision (Bob)
Model **B**: one record **per instance**, not per Message-ID. A copy of a message in
two folders is **two independent instances** with independent state (read/flag/star,
body, and — critically — **editable content**). Message-IDs are therefore **NOT globally
unique**; every code path that assumed "Message-ID → one row" must be made deliberate.
Bob's emphasis: **database integrity**.

## Identity
- **Instance = one `messages` row.** Natural key `(account_id, folder_id, uid)` (already
  the schema's UNIQUE). Each row keeps its own stable `uuid`, `flags_json`, `body_path`,
  and content (subject/body/etc. — may diverge once edited).
- `message_id` becomes a **non-unique indexed attribute**, used for threading and for
  *display* dedup only — never as an identity key.
- **Drop the move-detect Message-ID collapse.** A new `(folder, uid)` is ALWAYS its own
  instance, even when the same `message_id` exists in another folder.

## message_folders
Redundant under B (instance already = a `messages` row with folder_id/uid). It becomes
1:1 with `messages`. Plan: **retire it** — revert reads to `messages.folder_id`/`uid`,
removing the dual-representation integrity hazard. Staged so reads never break mid-flight.

## Server truth & move vs copy (reconcile)
- An instance exists iff the server lists `(folder, uid)`. Per-folder reconcile drops
  instances the server no longer confirms. This cleans accidental stale-move-sources
  while **preserving real copies** (server confirms both) — no exclusivity.
- **Server-side MOVE** (uid vanished from A, appeared in B, same message_id, A had exactly
  ONE such instance): transfer the old instance's local state (uuid, flags, body_path,
  local edits) to the new instance, then drop the old. Ambiguous case (copies present) →
  treat as a fresh instance + re-fetch. Edits survive unambiguous moves.

## Copy / Edit operations
- **Copy to folder** (user action or server-side sieve copy): a new instance. User copy
  queues a server `COPY`; sieve copy simply appears as a second server-confirmed instance.
- **Edit instance**: mutates only that instance's row + its own body file. No other
  instance is touched. (Whether edits push to the server is a separate question.)

## Display dedup
- Unified "All Inboxes" still collapses copies by `message_id` for display, but each
  instance is independently selectable/openable/editable.
- An **edited** instance (content diverged from its siblings) is shown **separately** —
  flag `edited=1` excludes it from the dedup group.

## No migration — rebuild
Per the standing rule (local store is a cache): **wipe local + re-sync** into the new
model rather than backfill 136k rows. Avoids fragile migration code. One full re-sync.

## Integrity safeguards (Bob's ask)
- `verifyInstances()` self-check on boot + a `-verify` command: every `messages` row has a
  valid folder; no two rows share `(account, folder, uid)`; `uuid` globally unique; every
  referenced `body_path` exists; `thread_id` resolvable. Log + count violations.
- All instance writes go through one chokepoint (`upsertInstance`) — no scattered INSERTs.
- Audit-log every state transfer on move and every reconcile drop (already partly done).

## Staged execution (each stage compiles, ships, is verifiable; no broken intermediates)
1. **Plan + integrity scaffold** (this doc) + `verifyInstances()` read-only checker (no
   behavior change) — measure current violations as a baseline.
2. **Stop the collapse**: `upsertMessage` creates a new instance per `(folder,uid)`;
   remove cross-folder rebind. Keep same-folder uid-update. Gate behind rebuild.
3. **Reconcile = server truth** per folder (drop unconfirmed; preserve copies); move-state
   transfer for the unambiguous single-instance move.
4. **Retire `message_folders`**: point reads back at `messages.folder_id`/`uid`; drop the
   join + its boot migration.
5. **Copy/Edit**: explicit copy-to-folder (new instance + server COPY) and per-instance
   edit; `edited` flag + display-dedup exclusion.
6. **Rebuild + verify** on the live DB; confirm copies preserved, accidents gone, 0
   integrity violations.

## Open sub-decisions (confirm before stage 2)
- **A.** Retire `message_folders` (recommended — kills the dual representation) vs keep it.
- **B.** Move-state transfer for unambiguous moves (recommended — preserves edits) vs
  always-fresh-instance on move (simpler, loses local read-state/edits on a server move).
- **C.** Do instance **edits push to the server** (APPEND new + delete old), or stay
  local-only annotations? (Affects whether an edit changes the server message_id.)
