# Persistence — In-Memory + JSON Snapshot (Q3 Default)

**Task**: AZ-668_mapobjects_store_persistence
**Name**: In-memory + JSON snapshot persistence (default per Q3)
**Description**: Crash-recovery and post-flight upload durability for the in-memory MapObjects state. Default engine: in-memory + atomic JSON snapshot to `${state_dir}/mapobjects/<mission_id>.json` per checkpoint. Q3 reserves the slot for SQLite+H3 / KV alternatives.
**Complexity**: 3 points
**Dependencies**: AZ-640_initial_structure, AZ-665_mapobjects_store_h3_classify, AZ-667_mapobjects_store_hydrate_and_pending
**Component**: mapobjects_store
**Tracker**: AZ-668
**Epic**: AZ-633

## Problem

The in-memory hashmap is authoritative for the active mission, but a crash mid-mission must not lose the pending diff. The persistence engine choice is Q3 (open); the default is in-memory + JSON snapshot (atomic rename), which keeps the engine choice cleanly behind a `MapObjectsPersistence` trait so SQLite+H3 or RocksDB can swap in later without touching call sites.

## Outcome

- `MapObjectsPersistence` trait with `save_snapshot(state) -> Result<()>` and `load_snapshot(path) -> Result<State>`.
- `JsonSnapshotEngine` impl that writes to `${state_dir}/mapobjects/<mission_id>.json` via atomic rename (write to `.tmp` then rename).
- Snapshot cadence: configurable; default every 30 s OR on every N pending-observation appends, whichever first.
- Crash recovery: at startup, load the most recent snapshot for any mission that did not reach `POST_FLIGHT_SYNC`.
- Health surface: `last_snapshot_ts`, `snapshot_size_bytes`, `snapshot_errors_total`.
- Persistence corruption on startup: refuse to start with stale state; surface explicit error to the operator.

## Scope

### Included
- `MapObjectsPersistence` trait.
- `JsonSnapshotEngine` (default impl).
- Atomic rename pattern.
- Crash-recovery load.
- Snapshot cadence policy.

### Excluded
- SQLite+H3 alternative (Q3 follow-up if chosen later).
- KV alternative (Q3 follow-up).
- The post-flight push itself (`mission_client` task 08).

## Acceptance Criteria

**AC-1: Snapshot + reload round-trip**
Given a store with 100 MapObjects + 10 IgnoredItems + 5 pending observations
When `save_snapshot()` writes to disk and a fresh process calls `load_snapshot()`
Then the loaded state equals the saved state.

**AC-2: Atomic rename prevents partial writes**
Given a snapshot write is interrupted mid-write (simulated kill -9)
When a fresh process starts
Then it loads the previous good snapshot, not the partial one (no corruption observed).

**AC-3: Crash recovery loads pending**
Given a previous run terminated with non-empty pending_observations
When the new process calls `load_snapshot()` for the same mission_id
Then pending_observations is non-empty and matches the pre-crash count.

**AC-4: Corruption surfaces explicit error**
Given a snapshot file with truncated content
When `load_snapshot()` runs
Then it returns `Err(CorruptSnapshot)` and `snapshot_errors_total` increments; the store does NOT silently start empty.

## Non-Functional Requirements

**Performance**
- Snapshot of a 30 km × 30 km mission (≤1 000 MapObjects): ≤1 s.
- Crash recovery: ≤2 s to a usable state (per `description.md §9`).

**Reliability**
- Atomic rename — no partial-write corruption.
- Corruption never silent.

## Runtime Completeness

- **Named capability**: persistent MapObjects state with crash recovery — default engine in-memory + JSON snapshot per Q3.
- **Production code that must exist**: real disk write; real atomic rename; real corruption-detection on load.
- **Allowed external stubs**: `tempfile` for test fixtures.
- **Unacceptable substitutes**: a no-op persistence in production is unacceptable (crash mid-flight loses the diff).
