mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 19:01:14 +00:00
880eabcb3f
Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
7.6 KiB
7.6 KiB
Contract: tile_downloader
Component: c11_tilemanager Producer task: AZ-316_c11_tile_downloader Consumer tasks: AZ-253 (E-C12 Operator Pre-flight Tooling — TBD at C12 decompose time) Version: 1.0.0 Status: draft Last Updated: 2026-05-10
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 GET 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.
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_areafor the same(bbox, zoom_levels, sector_class, flight_id)after a successful prior run is idempotent:outcome = idempotent_no_opand no GETs are issued. Idempotence is enforced by C11's download-progress journal undercache_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: TLS + service-internal API key authenticate the GET; auth failure surfaces as
SatelliteProviderErrorand aborts the run withoutcome = failure. The downloader does NOT fall back to plaintext or unauthenticated requests. - I-5: The downloader writes via the AZ-303
TileStore/TileMetadataStoreProtocols; it does NOT touch C6's filesystem layout directly. - I-6: A
CacheBudgetExceededErroraborts pre-write with no partial write andoutcome = 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-provideringest — owned byTileUploader(separate contract). - Not covered: parsing or validation of
satellite-provider's authentication payload beyond whathttpxprovides — 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.0.0 | 2026-05-10 | Initial contract — produced by AZ-316 (E-C11 decomposition) | autodev |