# Contract: tile_uploader

**Component**: c11_tilemanager
**Producer task**: AZ-319_c11_tile_uploader (initial), Batch 44 C11-SRP-revert (v2.0.0 gate removal)
**Consumer tasks**: AZ-329 (C12 `PostLandingUploadOrchestrator`) — see `_docs/02_document/contracts/c12_operator_orchestrator/` for the C12 surface that owns the post-landing safety gate.
**Version**: 2.0.0
**Status**: frozen
**Last Updated**: 2026-05-13

## Migration note — v1.0.0 → v2.0.0

Batch 44 removed C11's internal post-landing safety gate per SRP. v1.0.0 exposed `confirm_flight_state(): FlightStateSignal` and raised `FlightStateNotOnGroundError` from `upload_pending_tiles`. v2.0.0 drops both — the equivalent check moved to C12's `PostLandingUploadOrchestrator` (AZ-329), which inspects the C13 `flight_footer` FDR record and refuses to invoke `upload_pending_tiles` unless `clean_shutdown=True` is recorded. C11 is now a dumb pipe.

Consumers that still call `confirm_flight_state` or catch `FlightStateNotOnGroundError` MUST migrate to consuming C12's `FlightStateNotConfirmedError` family instead. ADR-004 process-level isolation remains the primary control — C11 never runs on the companion at all.

## Purpose

The `TileUploader` Protocol is C11's operator-side post-landing upload interface. C12's `PostLandingUploadOrchestrator` (AZ-329) invokes it during F10 (post-landing) AFTER it has confirmed `clean_shutdown=True` from the C13 `flight_footer` FDR record. C11 then reads mid-flight tiles flagged pending-upload from C6 (`source = onboard_ingest`, `voting_status = pending`), packages them per the D-PROJ-2 ingest contract sketch, signs each tile payload with the per-flight ephemeral key (AZ-318), and POSTs to `satellite-provider`'s `/api/satellite/tiles/ingest` endpoint. Acknowledged tiles are marked uploaded in C6.

C11 is operator-side ONLY; ADR-004 forbids the airborne companion image from importing this module.

## Shape

### Function / method API

```python
from typing import Protocol, runtime_checkable

@runtime_checkable
class TileUploader(Protocol):
    def upload_pending_tiles(self, request: UploadRequest) -> UploadBatchReport: ...
    def enumerate_pending_tiles(self, flight_id: uuid.UUID | None = None) -> list[TileMetadata]: ...
```

| Name | Signature | Throws / Errors | Blocking? |
|------|-----------|-----------------|-----------|
| `upload_pending_tiles` | `(request: UploadRequest) -> UploadBatchReport` | `SatelliteProviderError`, `RateLimitedError`, `SignatureRejectedError`, `TileMetadataError` | sync (post-landing; minutes) |
| `enumerate_pending_tiles` | `(flight_id: uuid.UUID \| None) -> list[TileMetadata]` | `TileMetadataError` | sync (seconds) |

### Data DTOs

```python
@dataclass(frozen=True)
class UploadRequest:
    flight_id:                  uuid.UUID | None         # None = all flights with pending
    batch_size:                 int                       # tiles per HTTP POST
    satellite_provider_url:     str                       # parent-suite ingest base URL

@dataclass(frozen=True)
class UploadBatchReport:
    batch_uuid:                 uuid.UUID                 # assigned by parent-suite ingest
    per_tile_status:            tuple[PerTileStatus, ...]
    retry_count:                int
    next_retry_at_s:            int | None                # set when partial-success
    outcome:                    UploadOutcome             # success | partial | failure
    public_key_fingerprint:     str                       # 16-hex; from AZ-318

@dataclass(frozen=True)
class PerTileStatus:
    tile_id:                    TileId                    # from c6_tile_cache
    status:                     IngestStatus              # queued | rejected | duplicate | superseded
    rejection_reason:           str | None
```

| Field | Type | Required | Description | Constraints |
|-------|------|----------|-------------|-------------|
| `UploadRequest.flight_id` | `UUID \| None` | no | Restricts batch to one flight | None = all pending across flights |
| `UploadRequest.batch_size` | `int` | yes | Tiles per HTTP POST | `1 ≤ batch_size ≤ 200` |
| `UploadBatchReport.batch_uuid` | `UUID` | yes | Parent-suite batch identifier | Server-assigned per D-PROJ-2 |
| `UploadBatchReport.per_tile_status` | `tuple[PerTileStatus, ...]` | yes | Per-tile result | Length = number of tiles attempted in this report |
| `UploadBatchReport.outcome` | `UploadOutcome` | yes | Aggregate outcome | `success` (all queued/duplicate/superseded) \| `partial` (some rejected/timeout) \| `failure` (gate blocked or full failure) |
| `UploadBatchReport.public_key_fingerprint` | `str` | yes | Identifies the per-flight signing key | 16 hex chars from AZ-318 |
| `PerTileStatus.status` | `IngestStatus` | yes | Server response status | `queued` \| `rejected` \| `duplicate` \| `superseded` |

## Invariants

- I-1 (v2.0.0): C11 itself does NOT gate on flight state. The pre-call gate is C12's `PostLandingUploadOrchestrator` (AZ-329), which inspects the C13 `flight_footer` FDR record for `clean_shutdown=True` BEFORE invoking `upload_pending_tiles`. C11 is a dumb pipe — once called, it proceeds to read C6 + POST to the satellite-provider with no internal short-circuit. ADR-004 process-level isolation remains the primary defence (C11 never runs on the companion).
- I-2: Every uploaded tile carries a signature produced by the AZ-318 per-flight key manager's `sign(payload)`. The parent suite verifies against the public key it received via the safety officer's pre-flight enrolment OR the `kind="c11.upload.session.key.public"` FDR record.
- I-3: A tile acknowledged as `queued`, `duplicate`, or `superseded` by the parent suite is marked `uploaded` in C6 (`mark_uploaded(tile_id)`); a tile acknowledged as `rejected` is NOT marked uploaded — it remains `pending` for human review.
- I-4: The per-flight signing key is zeroised at the end of `upload_pending_tiles` regardless of success or failure (try/finally in the caller; AZ-318's `end_session()`).
- I-5: A `SignatureRejectedError` from the parent suite triggers an FDR alert (AZ-318's `record_signature_rejection`); it is NEVER silently caught.
- I-6: The uploader writes via the AZ-303 `TileMetadataStore.mark_uploaded` Protocol; it does NOT update the metadata table directly.
- I-7: Partial-success batches are reported (not raised as failures) so the caller can re-invoke for the unacked tiles; idempotent retry behaviour is owned by the AZ-320 decorator that wraps this Protocol's impl.
- I-8: The signed payload includes `capture_timestamp` per the D-PROJ-2 contract sketch; the parent suite's nonce / timestamp validation owns replay defence.

## Non-Goals

- Not covered: airborne or in-flight uploads (RESTRICT-SAT-1 forbids them; airborne process cannot import this module per ADR-004).
- Not covered: orchestration of when the operator runs F10 — owned by C12.
- Not covered: tile downloads from `satellite-provider` — owned by `TileDownloader` (separate contract).
- Not covered: parent-suite voting / trust-promotion of uploaded tiles — owned by D-PROJ-2 design task #2 (`satellite-provider`).
- Not covered: HSM / TPM-backed key storage — out of scope this cycle (in-memory key with zeroisation).
- Not covered: mid-upload key rotation — one key per session.
- Not covered: idempotent retry across partial-success batches — separate task in this epic decorates this contract.

## Versioning Rules

- **Breaking changes** (renamed method, removed required field, changed return type, changed signature contract) require a major version bump. C12 is the sole consumer today; coordinate via Choose A/B/C/D when bumping.
- **Non-breaking additions** (new optional field on the report, new `IngestStatus` enum value the consumer already tolerates via `_ = status`) require a minor version bump.

## Test Cases

| Case | Input | Expected | Notes |
|------|-------|----------|-------|
| upload-happy-path | 50 pending tiles, parent-suite returns 202 with all `queued` | `UploadBatchReport.outcome = success`; all 50 marked `uploaded` in C6; signature verifies on each | C11-IT-03 |
| post-landing-gate-in-c12 | C12 `PostLandingUploadOrchestrator` invocation flow | The flight-state gate lives in C12 (`FlightStateNotConfirmedError`), not C11. v2.0.0 removed the C11 internal gate. | See `c12_operator_orchestrator` contract + AZ-329 spec |
| signature-rejected | Parent suite returns `rejected` for 1 tile with reason `"invalid signature"` | `PerTileStatus.status = rejected`; `outcome = partial`; FDR `c11.upload.signature_rejected` emitted; the tile NOT marked uploaded | I-5 |
| duplicate-acknowledged | Parent suite returns `duplicate` for 5 tiles (already ingested in a prior batch) | All 5 marked `uploaded`; `outcome = success` | I-3 |
| signing-key-zeroised | Run a successful upload, then assert the AZ-318 manager's `_private_key is None` | Always zeroised; FDR `c11.upload.session.key.zeroised` recorded | I-4 |
| signing-key-zeroised-on-failure | Network drop mid-batch raises `SatelliteProviderError`, then assert key zeroised | Always zeroised even on failure | I-4 |
| empty-pending-set | No pending tiles | `outcome = success` with empty `per_tile_status`; zero POSTs; zero key generation | edge case |
| public-key-in-fdr-before-first-post | Capture FDR records | `kind="c11.upload.session.key.public"` precedes any `c11.upload.tile.*` records | safety-officer correlation |

## Change Log

| Version | Date | Change | Author |
|---------|------|--------|--------|
| 1.0.0 | 2026-05-10 | Initial contract — produced by AZ-319 (E-C11 decomposition) | autodev |
| 2.0.0 | 2026-05-13 | Batch 44: remove C11 internal flight-state gate per SRP. `confirm_flight_state` method dropped; `FlightStateNotOnGroundError` retired; post-landing safety gate now owned by C12's `PostLandingUploadOrchestrator` (AZ-329). Breaking — consumers MUST migrate to C12's `FlightStateNotConfirmedError`. | autodev (Batch 44) |
