mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 15:41:12 +00:00
Decompose Step 6 snapshot: 140 task specs + contract docs
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>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
@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: TLS + service-internal API key authenticate the GET; auth failure surfaces as `SatelliteProviderError` and aborts the run with `outcome = failure`. The downloader does NOT fall back to plaintext or unauthenticated requests.
|
||||
- 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.0.0 | 2026-05-10 | Initial contract — produced by AZ-316 (E-C11 decomposition) | autodev |
|
||||
Reference in New Issue
Block a user