[AZ-319] C11 HttpTileUploader (post-landing upload path)

Lands the production HttpTileUploader composing AZ-317's gate, AZ-318's
per-flight signing, and consumer-side cuts over c6 storage. Implements
the full upload flow: gate ON_GROUND -> start_session -> enumerate
pending -> per-batch multipart POST with Ed25519 signing -> mark_uploaded
on ack -> end_session in finally. Honours Retry-After (RFC 7231 int +
HTTP-date), exponential backoff on 5xx, fail-fast on TLS/401/403.

Adds C11Config block, three FDR kinds (tile.queued, tile.rejected,
batch.complete), and the build_tile_uploader composition-root factory.
Cross-component access to c6 stays Protocol-cut (AZ-507 / AZ-270).

Tests: 17 new unit tests covering AC-1..AC-14 plus throughput NFR; AZ-272
schema fixtures for the three new FDR kinds. Full unit suite: 1404 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 06:13:36 +03:00
parent cde237e236
commit 610e8a743c
15 changed files with 2461 additions and 24 deletions
@@ -0,0 +1,150 @@
# Batch 39 — Cycle 1 Report
**Date**: 2026-05-13
**Batch**: 39 (single-task batch — C11 upload orchestrator)
**Tasks**:
- AZ-319 (C11 TileUploader, 5pt)
**Total complexity**: 5pt
**Status**: complete; pending transition to "In Testing".
## Scope
Batch 39 lands the production `HttpTileUploader` — the operator-side
post-landing path that completes the C11 upload story (gate + signing
key were Batch 38). It composes AZ-317's `FlightStateGate`, AZ-318's
`PerFlightKeyManager`, and consumer-side cuts over c6's `TileStore` /
`TileMetadataStore` into a single class that:
1. Gates on `ON_GROUND` BEFORE any C6 read or HTTP egress
2. Starts an AZ-318 signing session (deterministic per-flight Ed25519)
3. Enumerates pending tiles from c6 (`source = onboard_ingest`,
`voting_status = pending`), batched to `request.batch_size`
4. Per batch: reads pixel bytes, computes the canonical signing
payload (SHA-256 over `tile_blob || zoom || lat || lon ||
capture_ts_ns || flight_id || companion_id || quality_json`),
signs it, packages a multipart POST per the D-PROJ-2 contract
sketch, and submits to `/api/satellite/tiles/ingest`
5. Honours `Retry-After` on 429s (RFC 7231 integer-seconds AND
HTTP-date forms), backs off exponentially on 5xx (1s/2s/4s/8s),
fails fast on TLS / 401 / 403
6. Marks acked tiles `uploaded` in c6 (one stamp per acknowledged
tile, with the per-batch `batch_uuid` as the audit correlation key)
7. Surfaces per-tile signature rejections through
`key_manager.record_signature_rejection` AND a dedicated
`c11.upload.tile.rejected` FDR record
8. Always calls `key_manager.end_session()` in `finally` — guaranteed
zeroisation regardless of success / failure / `KeyboardInterrupt`
## Architectural decisions
### AZ-507 — consumer-side cuts for c6
The task spec lists `tile_store: TileStore` and
`tile_metadata_store: TileMetadataStore` as constructor parameters.
A direct `from gps_denied_onboard.components.c6_tile_cache import …`
would violate AZ-507 (cross-component imports forbidden) and trip
the AZ-270 lint. Instead, `tile_uploader.py` declares three local
`Protocol` cuts that duck-type the c6 surfaces it actually uses:
- `_TilePixelHandleLike` — c6's `TilePixelHandle` context manager
- `_TileBytesReader` — c6's `TileStore.read_tile_pixels(tile_id)`
- `_PendingMetadataReader` — c6's `TileMetadataStore.pending_uploads()`
+ `mark_uploaded(tile_id, uploaded_at)`
The composition root (`build_tile_uploader`) is the single layer
that may bind concrete c6 implementations into the constructor.
This pattern is documented in
`_docs/02_document/module-layout.md` Rule 9 and was already used
for `FlightStateSource` in AZ-317.
### Sleep injection vs. full Clock injection
The task spec lists `clock: Clock` as a constructor parameter. The
uploader only ever needs a sleep primitive (for 429 / 5xx backoff),
never `monotonic_ns` or `time_ns`. Threading the full `Clock`
Protocol through would carry payload the class never reads.
Implementation accepts a `sleep: Callable[[float], None]` defaulting
to a `WallClock`-routed helper, which preserves the AZ-398 invariant
that `components/` never calls `time.sleep` directly. Documented in
the batch review as F2 (Low).
### FDR key naming
The three new `KNOWN_PAYLOAD_KEYS` entries
(`c11.upload.tile.queued`, `c11.upload.tile.rejected`,
`c11.upload.batch.complete`) carry consistent correlation keys
(`flight_id`, `fingerprint`, `batch_uuid`, `observed_at_iso`)
across all three records, so an auditor can join per-tile events
to the batch summary and back to the `c11.upload.session.key.public`
record from Batch 38. Per-tile records also carry the `IngestStatus`
enum value as `status` for fast filtering.
### Failure paths raise vs. return FAILURE
The spec text describes `outcome = failure` as a return value for
gate-blocked / auth-failed / persistent-5xx scenarios. The
implementation raises (`FlightStateNotOnGroundError`,
`SatelliteProviderError`, `RateLimitedError`) instead and the
`finally` emitter writes `outcome = failure` into the FDR
`c11.upload.batch.complete` record. AC-2, AC-9, AC-10 all assert
the raise behaviour, so the spec text drift is documented in the
batch review (F1, Low) without code change.
## Files touched
Production:
- `src/gps_denied_onboard/components/c11_tile_manager/_types.py`
(added `IngestStatus`, `UploadOutcome`, `UploadRequest`,
`PerTileStatus`, `UploadBatchReport`)
- `src/gps_denied_onboard/components/c11_tile_manager/errors.py`
(added `SatelliteProviderError`, `RateLimitedError`)
- `src/gps_denied_onboard/components/c11_tile_manager/config.py` (new)
- `src/gps_denied_onboard/components/c11_tile_manager/interface.py`
(`TileUploader` Protocol now has the real signature)
- `src/gps_denied_onboard/components/c11_tile_manager/tile_uploader.py` (new)
- `src/gps_denied_onboard/components/c11_tile_manager/__init__.py`
(re-exports + `register_component_block`)
- `src/gps_denied_onboard/runtime_root/c11_factory.py`
(added `build_tile_uploader`)
- `src/gps_denied_onboard/fdr_client/records.py`
(3 new `KNOWN_PAYLOAD_KEYS` entries)
Tests:
- `tests/unit/c11_tile_manager/test_tile_uploader.py` (new — 15 tests)
- `tests/unit/c11_tile_manager/test_protocol_conformance.py` (new — 2 tests)
- `tests/unit/test_az272_fdr_record_schema.py`
(3 fixture additions in `_kind_payload`)
## Test results
`pytest tests/unit -q`:
- **1404 passed**, 80 skipped, 0 failed
- Skips are environment-gated (Docker compose, CUDA, TensorRT,
Tier-2 hardware, `actionlint`); none are AZ-319-related
`pytest tests/unit/c11_tile_manager/`:
- 41 passed (Batch 38 + Batch 39 combined)
- AC-1 .. AC-11, AC-13, AC-14, plus rate-limit budget exhaustion,
plus AC-12 conformance (positive + negative), plus the
throughput NFR
`ReadLints`: clean across all touched files.
## Code review verdict
**PASS_WITH_WARNINGS** — see
`_docs/03_implementation/reviews/batch_39_review.md`. Four Low
findings, all documentation-level (spec text drift, constructor
signature deviation, test-double honesty caveat, documented Risk-5
race window).
## Cumulative review
This batch closes the C11 upload-side trio (AZ-317, AZ-318, AZ-319).
The next cumulative review window covers batches 37-39; that
report will land before Batch 41 starts.