# Pre-Flight Hydrate + Sync State Machine + Pending Logs

**Task**: AZ-667_mapobjects_store_hydrate_and_pending
**Name**: Pre-flight hydrate from MapObjectsBundle + sync_state machine + pending_observations/pending_ignored append logs
**Description**: Hydrate the store from a `MapObjectsBundle` (from `mission_client`'s pull). Maintain a `sync_state` enum (`synced | cached_fallback | degraded | failed`). Append every NEW / MOVED / EXISTING / REMOVED-CANDIDATE / IgnoredItem event to `pending_observations` / `pending_ignored` for the post-flight push.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-665_mapobjects_store_h3_classify, AZ-666_mapobjects_store_ignored_and_pass_sweep
**Component**: mapobjects_store
**Tracker**: AZ-667
**Epic**: AZ-633

## Problem

The on-device working copy is hydrated pre-flight from the central API. The sync_state machine (`fresh_boot → synced | cached_fallback | degraded`) tracks the relationship to the central source of truth. During flight, every classification event is appended to `pending_observations` (or, for declines, `pending_ignored`) — central writes are forbidden mid-flight (Frozen choice 6). The pending logs feed the post-flight push.

## Outcome

- `hydrate(bundle: MapObjectsBundle) -> Result<()>` loads the bundle into the in-memory hashmap + IgnoredSet; sets `sync_state = synced` (or `cached_fallback` if `bundle.fallback_used`).
- `on_classify_result(classification, detection)` appends a `MapObjectObservation` to `pending_observations` for NEW / MOVED / EXISTING / REMOVED-CANDIDATE.
- `on_decline(ignored_item)` appends to `pending_ignored`.
- `drain_pending() -> (Vec<MapObjectObservation>, Vec<IgnoredItem>)` is called by `mission_client::push_mapobjects_diff` post-flight.
- Health surface: `sync_state`, `pending_observations_count`, `pending_ignored_count`, `last_pull_ts`, `last_push_ts`.
- On `DELETE /missions/{id}` cascade signal from `mission_client`, drop mission-scoped objects.

## Scope

### Included
- `MapObjectsBundle` hydration (model = `data_model.md §MapObjectsBundle`).
- Sync-state enum + transitions.
- Append-only `pending_observations` + `pending_ignored` logs (in-memory; durable disk handoff lives in `mission_client` task 08).
- Drain API.
- Mission-cascade handler.

### Excluded
- H3 classify (task 26).
- Disk persistence (task 29) — this task keeps pending in memory + lets `mission_client` task 08 handle disk durability.
- Post-flight push (lives in `mission_client` task 08).

## Acceptance Criteria

**AC-1: Hydrate from bundle**
Given a `MapObjectsBundle` with N MapObjects and M IgnoredItems
When `hydrate(bundle)` is called
Then the store contains all N + M entries and `sync_state = "synced"`.

**AC-2: Fallback bundle sets cached_fallback**
Given a bundle with `fallback_used = true`
When `hydrate(bundle)` is called
Then `sync_state = "cached_fallback"`.

**AC-3: Classify appends pending observation**
Given the store hydrated and a detection that classifies as `New`
When `on_classify_result(New, detection)` is called
Then `pending_observations_count` increments by 1.

**AC-4: Drain returns and clears pending**
Given pending_observations_count = 5, pending_ignored_count = 2
When `drain_pending()` is called
Then it returns 5 observations + 2 ignored items; counts return to 0.

**AC-5: Cascade drops mission-scoped objects**
Given `M1` (mission A) and `M2` (mission B) objects in the store
When the cascade signal for mission A arrives
Then `M1` is dropped; `M2` remains.

## Non-Functional Requirements

**Performance**
- Hydrate from a 30 km × 30 km bundle: ≤2 s (peer of pre-flight pull's 30 s budget).
- Append per classification: ≤100 µs.

## Contract

- Canonical typed model: `data_model.md §MapObjectsBundle`, `§MapObjectObservation`.

## Runtime Completeness

- **Named capability**: hydrate + sync_state + pending event logs.
- **Production code that must exist**: real hydrate; real pending append; real drain.
- **Unacceptable substitutes**: central writes mid-flight are forbidden (Frozen choice 6).
