# Contract: RouteTileDelivery (gRPC)

**Component**: c11_tilemanager (consumer), satellite-provider (producer)
**Epic**: AZ-976
**ADR**: ADR-013 (architecture.md)
**Proto**: `tile_provision.proto` — `package satellite.v1`
**Version**: 0.3.0
**Status**: proposed
**Last Updated**: 2026-06-19

## Purpose

Operator-side **pre-flight cache provisioning**. Client sends route + onboard tile catalog once; server streams `RouteTileEvent` messages until `DeliveryComplete` or `DeliveryError`.

satellite-provider does **not** receive `flight_id` — that is a C6 bookkeeping concern on the gps-denied side only (`route_id` is the wire correlation id).

C11/C12 on the **operator workstation** only. ADR-004: airborne image must not import stubs or open this channel.

## RPC

```protobuf
service RouteTileDelivery {
  rpc DeliverRouteTiles(DeliverRouteTilesRequest) returns (stream RouteTileEvent);
}
```

| Concern | Rule |
|---------|------|
| Auth | gRPC metadata `authorization: Bearer <JWT>` |
| TLS | Required in production; `SATELLITE_PROVIDER_TLS_INSECURE=1` dev knob |
| Idempotency | `RouteSpec.route_id` (UUID string) |
| Resume | Client persists last acked `batch_seq` per `route_id` locally (not on wire) |

## Request

### `DeliverRouteTilesRequest`

| Field | Description |
|-------|-------------|
| `route` | Corridor geometry + single zoom |
| `client_tiles` | Onboard inventory snapshot (route intersection only) |

### `RouteSpec`

| Field | Maps from gps-denied |
|-------|----------------------|
| `route_id` | Client-generated UUID per provision job |
| `waypoints` | `replay_input.tlog_route.RouteSpec.waypoints` |
| `region_size_meters` | `RouteSpec.suggested_region_size_meters` |
| `zoom` | Single slippy zoom level (confirmed sufficient) |
| `geofences` | Optional inclusion polygons |
| `include_geofence_tiles` | Union geofence tiles with corridor grid |

### `ClientTileRecord`

Canonical key: **`(z, x, y)`**. `source` is informational only — **not** used in skip logic.

| Field | C6 mapping |
|-------|------------|
| `resolution_m_per_px` | RESTRICT-SAT-4 (lower = better) |
| `captured_at` | `TileMetadata.capture_timestamp` |
| `content_sha256` | `TileMetadata.content_sha256_hex` (raw 32 bytes) |

## Server skip rule (client catalog)

For each server candidate tile, **omit from stream** when `client_tiles` has matching `(z,x,y)` and **any** of:

1. `client.content_sha256` is non-empty and **equals** server payload hash → skip (byte-identical)
2. `client.resolution_m_per_px <= server.resolution_m_per_px` **and** `client.captured_at >= server.captured_at` → skip (metadata-sufficient)

`source` is **not** compared.

`RouteManifest.skipped_by_client` counts tiles removed by this rule.

## Sector — not on this wire

**Sector** (`active_conflict` vs `stable_rear`) controls **how stale a tile may be before C6 rejects it on write** (AC-NEW-6 freshness). It is an operator decision about the geographic area, not something satellite-provider needs to deliver tiles.

| Layer | Who applies sector |
|-------|-------------------|
| satellite-provider | Does not need sector — streams tiles by route geometry |
| C11 client write | Reads sector from **C11/C12 config** (same as today) when calling C6 freshness gate |

No `SectorClass` field on the gRPC request.

## Response stream: `RouteTileEvent`

Typical sequence:

1. **`RouteManifest`** — `total_candidates`, `skipped_by_client`, `to_deliver`
2. **`TileBatch`** — monotonic `batch_seq`; on-disk hits first, then freshly fetched
3. **`ProgressUpdate`** — optional
4. **`DeliveryComplete`** or **`DeliveryError`**

### `DeliveryComplete` counters

| Field | Meaning |
|-------|---------|
| `delivered` | Tiles actually sent in `TileBatch` streams |
| `skipped_client` | Same as manifest `skipped_by_client` (echo for client verify) |
| `skipped_server_filter` | Tiles SP required but **did not send** after client dedup — see below |

#### `skipped_server_filter` — what counts

Tiles that entered the post-client-dedup work queue but never appeared in a batch:

| Reason | Example |
|--------|---------|
| **Fetch failed** | External imagery provider 404/timeout after retries |
| **Below SP min resolution** | SP refuses to store/serve below its configured floor |
| **Geometry clip** | Tile dropped after server-side corridor/geofence validation |
| **Operational cap** | Job hit max-tiles / rate limit (if SP enforces) |

Tiles skipped by the **client catalog rule** are **not** included here (they are `skipped_client`).

If SP has no server-side filters in v1, `skipped_server_filter` may be **0**; the field is reserved for observability.

### `TilePayload`

| Field | Notes |
|-------|-------|
| `content_sha256` | 32-byte SHA-256 of `jpeg`; matches C6 DB invariant |
| `route_priority` | Lower = earlier along route |

## Client write path (gps-denied)

`RouteTileDeliveryClient` (C11):

- Assigns C6 `flight_id` from operator context locally (not from SP)
- Applies RESTRICT-SAT-4, **sector-based freshness**, AZ-308 budget, download journal
- Resumes via persisted `route_id` + `batch_seq`

## Migration

REST `route_client` + `HttpTileDownloader` remain fallback until AZ-979 benchmark.

## Change log

| Version | Date | Change |
|---------|------|--------|
| 0.3.0 | 2026-06-19 | `ClientTileRecord.content_sha256`; sequential field nums on `TilePayload`; sector/flight_id off wire; skip rule + `skipped_server_filter` defined |
| 0.2.0 | 2026-06-19 | `satellite.v1.RouteTileDelivery` + `RouteTileEvent` oneof |
| 0.1.0 | 2026-06-19 | Initial draft (superseded) |
