Files
Oleksandr Bezdieniezhnykh 940066bee2 chore: WIP pre-implement
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 17:09:13 +03:00

9.1 KiB

Contract: tile_downloader

Component: c11_tilemanager Producer task: AZ-316_c11_tile_downloader (initial), AZ-777 Phase 1 (cycle-3 inventory-contract adaptation) Consumer tasks: AZ-253 (E-C12 Operator Pre-flight Tooling — TBD at C12 decompose time) Version: 1.1.0 Status: stable Last Updated: 2026-05-26

Purpose

The TileDownloader Protocol is C11's operator-side download interface. C12 invokes it during F1 (pre-flight cache build) to fetch satellite tiles from the parent suite's satellite-provider inventory + slippy-map surface, apply RESTRICT-SAT-4 resolution gating at the C11 boundary, and write accepted tiles into C6. Freshness rejections surfacing from C6 (AZ-307) are counted and surfaced in the report.

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

Upstream API (cycle 3 — AZ-777 Phase 1): against the real parent-suite satellite-provider v1.0.0 inventory contract — POST /api/satellite/tiles/inventory (bulk lookup by (zoom, x, y), ≤ 5000 entries / request, per tile-inventory.md v1.0.0 / AZ-505) + GET /tiles/{z}/{x}/{y} (slippy-map JPEG fetch, issued only for inventory entries with present=true). Authentication: Authorization: Bearer ${SATELLITE_PROVIDER_API_KEY}; the dev-only SATELLITE_PROVIDER_TLS_INSECURE=1 env knob accepts the self-signed dev cert (production must validate against a CA-issued cert). Because the inventory response carries no Content-Length hint, AZ-308's pre-write budget pre-check uses a conservative _DEFAULT_ESTIMATED_TILE_BYTES = 50 000 per-tile reserve.

Shape

Function / method API

from typing import Protocol, runtime_checkable
from pathlib import Path

@runtime_checkable
class TileDownloader(Protocol):
    def download_tiles_for_area(self, request: DownloadRequest) -> DownloadBatchReport: ...
    def enumerate_remote_coverage(self, bbox: Bbox, zoom_levels: list[int]) -> list[TileSummary]: ...
Name Signature Throws / Errors Blocking?
download_tiles_for_area (request: DownloadRequest) -> DownloadBatchReport SatelliteProviderError, RateLimitedError, ResolutionRejectionError, CacheBudgetExceededError, TileFsError, TileMetadataError sync (offline; minutes)
enumerate_remote_coverage (bbox: Bbox, zoom_levels: list[int]) -> list[TileSummary] SatelliteProviderError, RateLimitedError sync (seconds)

Data DTOs

@dataclass(frozen=True)
class DownloadRequest:
    bbox:                       Bbox                     # from c6_tile_cache
    zoom_levels:                tuple[int, ...]
    sector_class:               SectorClassification     # from c6_tile_cache
    satellite_provider_url:     str                      # parent-suite base URL
    service_api_key:            str                      # TLS + service-internal
    cache_root:                 Path                     # operator workstation
    flight_id:                  uuid.UUID                # tags downloads in C6 metadata

@dataclass(frozen=True)
class DownloadBatchReport:
    tiles_downloaded:           int
    tiles_rejected_freshness:   int                      # raised by AZ-307 at C6 boundary
    tiles_rejected_resolution:  int                      # rejected by C11 (RESTRICT-SAT-4)
    tiles_downgraded:           int                      # stable_rear stale → DOWNGRADED label
    freshness_summary:          dict[FreshnessLabel, int]
    outcome:                    DownloadOutcome          # success | failure | idempotent_no_op
    failure_reason:             str | None

@dataclass(frozen=True)
class TileSummary:
    tile_id:                    TileId                   # from c6_tile_cache
    produced_at:                datetime
    resolution_m_per_px:        float
    estimated_bytes:            int
Field Type Required Description Constraints
DownloadRequest.bbox Bbox yes Operational area min_lat ≤ max_lat, min_lon ≤ max_lon
DownloadRequest.zoom_levels tuple[int, ...] yes Zoom levels to fetch each in [0, 21]; deduplicated
DownloadRequest.sector_class SectorClassification yes Drives freshness rule applied at C6 ACTIVE_CONFLICT | STABLE_REAR
DownloadRequest.cache_root Path yes Operator workstation cache dir must exist; must be writable
DownloadBatchReport.tiles_downloaded int yes Tiles written to C6 successfully ≥ 0
DownloadBatchReport.tiles_rejected_resolution int yes Tiles rejected at C11 boundary for < 0.5 m/px ≥ 0
DownloadBatchReport.tiles_rejected_freshness int yes Count of FreshnessRejectionError raised by C6 (AZ-307) ≥ 0
DownloadBatchReport.outcome DownloadOutcome yes Aggregate outcome enum

Invariants

  • I-1: tiles_downloaded + tiles_rejected_resolution + tiles_rejected_freshness == sum of attempted tiles. The report accounts for every tile the downloader attempted; no silent drops.
  • I-2: A re-run of download_tiles_for_area for the same (bbox, zoom_levels, sector_class, flight_id) after a successful prior run is idempotent: outcome = idempotent_no_op and no GETs are issued. Idempotence is enforced by C11's download-progress journal under cache_root/.c11/journal/.
  • I-3: Every accepted tile passes BOTH the C11 resolution gate (≥ 0.5 m/px per RESTRICT-SAT-4) AND the C6 freshness gate (AZ-307). A tile that fails either is excluded from tiles_downloaded.
  • I-4: JWT Bearer authentication (SATELLITE_PROVIDER_API_KEY) over TLS authenticates the inventory POST and the slippy-map GET; auth failure surfaces as SatelliteProviderError and aborts the run with outcome = failure. The downloader does NOT fall back to plaintext or unauthenticated requests. SATELLITE_PROVIDER_TLS_INSECURE=1 is a dev-only knob for self-signed certs; production must run with it unset.
  • I-5: The downloader writes via the AZ-303 TileStore/TileMetadataStore Protocols; it does NOT touch C6's filesystem layout directly.
  • I-6: A CacheBudgetExceededError aborts pre-write with no partial write and outcome = failure. The C6 cache budget enforcer (AZ-308) drives the headroom check.

Non-Goals

  • Not covered: airborne or in-flight downloads (RESTRICT-SAT-1 forbids them; airborne process cannot import this module per ADR-004).
  • Not covered: orchestration of when the operator runs F1 — owned by C12.
  • Not covered: cache artifact build (descriptors, FAISS index) — owned by C10 after the downloader populates C6.
  • Not covered: tile uploads to satellite-provider ingest — owned by TileUploader (separate contract).
  • Not covered: parsing or validation of satellite-provider's authentication payload beyond what httpx provides — out of scope for the onboard side.

Versioning Rules

  • Breaking changes (renamed method, removed required field, changed return type) 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 error variant the consumer already catches via the family) require a minor version bump.

Test Cases

Case Input Expected Notes
download-happy-path DownloadRequest for Derkachi bbox with mix of fresh active_conflict + stable_rear tiles DownloadBatchReport with tiles_downloaded > 0; sum of report counts equals attempt count; tiles present in C6 C11-IT-01
freshness-rejection-counts source returns stale tiles in active_conflict sector DownloadBatchReport.tiles_rejected_freshness > 0; matches C6's AZ-307 rejection count for that batch C11-IT-02
resolution-gate-rejects source returns tile with resolution_m_per_px = 0.3 (< 0.5) tile excluded from tiles_downloaded; tiles_rejected_resolution += 1; no C6 write attempted RESTRICT-SAT-4
auth-failure-aborts invalid service_api_key first GET raises SatelliteProviderError; outcome = failure; no tiles written I-4
budget-exceeded-aborts pre-write check shows insufficient headroom CacheBudgetExceededError; outcome = failure; zero partial writes I-6
idempotent-rerun second call with identical request after success outcome = idempotent_no_op; zero GETs observed I-2
rate-limited-honors-retry-after source returns 429 with Retry-After: 30 downloader sleeps ≥ 30s before retry; no RateLimitedError raised on success path RFC 6585

Change Log

Version Date Change Author
1.1.0 2026-05-26 Internal upstream contract adapted to satellite-provider v1.0.0 inventory contract (AZ-777 Phase 1): POST /api/satellite/tiles/inventory + GET /tiles/{z}/{x}/{y} replace the previous GET /api/satellite/tiles?bbox=…&zoom=… shape. download_tiles_for_area / DownloadRequest / DownloadBatchReport surface UNCHANGED — non-breaking minor bump. Auth tightened to JWT Bearer over TLS. Status moved draft → stable. autodev
1.0.0 2026-05-10 Initial contract — produced by AZ-316 (E-C11 decomposition) autodev