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

11 KiB
Raw Permalink Blame History

Contract: route_client

Component: c11_tilemanager Producer task: AZ-838_satellite_provider_route_client (Epic AZ-835 C2) Consumer tasks: AZ-839 (operator_pre_flight_setup real fixture, Epic AZ-835 C3); AZ-840 (E2E orchestrator test, Epic AZ-835 C4); future C12 production binding (deferred — see § Non-Goals). Version: 1.0.0 Status: stable Last Updated: 2026-05-26

Purpose

The SatelliteProviderRouteClient is C11's operator-side route-onboarding interface. Given a RouteSpec (a coarsened, tlog-derived flight corridor produced by replay_input.tlog_route.extract_route_from_tlog — AZ-836), it registers the corridor with the parent-suite satellite-provider Route API, polls until materialisation completes, and verifies coverage via the inventory contract.

The route-driven seeding flow lets the operator pre-commit the C6 cache to the precise corridor the drone actually flew rather than a coarse bounding box — typically ~100× more tile-efficient on long, narrow flights.

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

Upstream API (cycle 3 — AZ-838): POST /api/satellite/route (corridor onboarding; body shape per CreateRouteRequest.cs / RoutePoint.cs / GeoPoint.cs DTOs; query requestMaps=true&createTilesZip=false) + GET /api/satellite/route/{id} (status polling; terminal-success when mapsReady=true; terminal-failure when status ∈ {failed, error, rejected}) + POST /api/satellite/tiles/inventory (post-materialisation coverage verification, shared with tile_downloader). Authentication: Authorization: Bearer ${SATELLITE_PROVIDER_API_KEY}; the dev-only SATELLITE_PROVIDER_TLS_INSECURE=1 env knob accepts the self-signed dev cert.

Shape

Function / method API

import uuid
from gps_denied_onboard._types.route import RouteSpec   # AZ-845 canonical home

class SatelliteProviderRouteClient:
    def __init__(
        self,
        base_url: str,
        jwt: str,
        *,
        tls_insecure: bool = False,
        request_timeout_s: float = 30.0,
        poll_interval_s: float = 5.0,
        poll_max_attempts: int = 60,
    ) -> None: ...

    def seed_route(
        self,
        spec: RouteSpec,
        *,
        name: str | None = None,
    ) -> RouteSeedResult: ...
Name Signature Throws / Errors Blocking?
seed_route (spec: RouteSpec, *, name: str | None = None) -> RouteSeedResult RouteValidationError, RouteTransientError, RouteTerminalFailureError (all under SatelliteProviderRouteError) sync; poll loop bounded by poll_max_attempts × poll_interval_s (default 60 × 5 s = 5 min ceiling)

Data DTOs

@dataclass(frozen=True, slots=True)
class RouteSpec:                                          # _types/route.py (AZ-845)
    waypoints:                       tuple[tuple[float, float], ...]   # (lat, lon)
    suggested_region_size_meters:    float                              # per-waypoint coverage radius
    source_tlog:                     Path                               # provenance
    source_segment:                  tuple[int, int]                    # (start_idx, end_idx) into tlog GPS rows
    total_distance_meters:           float                              # along-track distance of active segment

@dataclass(frozen=True, slots=True)
class RouteSeedResult:                                    # c11_tile_manager/route_client.py
    route_id:                         uuid.UUID
    terminal_status:                  str                # e.g. "completed", "done", "succeeded"
    maps_ready:                       bool               # True on terminal success
    tile_count:                       int                # present=true entries from inventory verify
    elapsed_ms:                       int                # POST → terminal-status wall time
    submitted_payload_sha256:         str                # provenance for the inventory verify step
Field Type Required Description Constraints
RouteSpec.waypoints tuple[tuple[float, float], ...] yes Ordered list of (lat, lon) waypoints 2 ≤ len(waypoints) ≤ 500 (AZ-809 validator); each lat ∈ [-90, 90], lon ∈ [-180, 180]
RouteSpec.suggested_region_size_meters float yes Per-waypoint coverage radius 100.0 ≤ value ≤ 10_000.0 (AZ-809 validator)
RouteSpec.source_tlog Path yes Provenance — which tlog produced this spec filesystem path
RouteSeedResult.route_id uuid.UUID yes Server-assigned route id non-zero
RouteSeedResult.terminal_status str yes Last status observed from GET /api/satellite/route/{id} one of {"completed", "failed", "error", "done", "succeeded", "rejected"}
RouteSeedResult.maps_ready bool yes True iff parent suite reported mapsReady=true (terminal success) True on success; False if poll budget exhausted before terminal
RouteSeedResult.tile_count int yes Inventory present=true count over the route's enumerated coverage ≥ 0 (lower bound — server may interpolate between waypoints)

Invariants

  • I-1: Pre-emptive validation rejects obviously-bad input as RouteValidationError BEFORE the HTTP POST. The client mirrors the AZ-809 CreateRouteRequestValidator bounds (points 2..500; regionSizeMeters 100..10 000; zoomLevel 0..22; lat/lon ranges; name/description max lengths). The list MUST stay in sync with SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs (parent suite source).
  • I-2: The client POSTs the wire shape exactly per CreateRouteRequest.cs + RoutePoint.cs + GeoPoint.cs (note: RoutePoint uses lat / lon JSON property names for both input and output; the input/output naming asymmetry flagged in AZ-809 AC-10 is a parent-suite concern, not a client adaptation).
  • I-3: Poll cadence MUST respect poll_interval_s (lower bound between successive GET /api/satellite/route/{id} calls) and poll_max_attempts (upper bound on attempt count). The client logs every poll tick at INFO with the observed status.
  • I-4: Terminal-success is exactly mapsReady=true. Terminal-failure is exactly status ∈ {"failed", "error", "rejected"}. Any other status is treated as "still processing" and triggers the next poll. If the poll budget is exhausted without terminal status, RouteTransientError is raised with the last observed status.
  • I-5: 4xx responses with RFC 7807 ProblemDetailsRouteValidationError; field_errors is populated from the errors dict so the caller can render per-field rejections.
  • I-6: 5xx / network / timeout → RouteTransientError with __cause__ set to the underlying httpx exception. The retry semantics are caller-driven — the route client itself does NOT retry the POST, leaving the policy to the fixture / CLI (e.g., tests/e2e/replay/conftest.py::operator_pre_flight_setup retries up to 3 times using C11's _DEFAULT_BACKOFF_SCHEDULE_S = (1, 2, 4, 8)).
  • I-7: The inventory verify step uses POST /api/satellite/tiles/inventory (≤ 5000 entries / request) and enumerates the route's tile coverage locally from (waypoints, suggested_region_size_meters) using the parent suite's web-Mercator math (_EARTH_EQUATORIAL_CIRCUMFERENCE_M = 40 075 016.686). The result is a lower bound on actual server coverage — the server may interpolate intermediate corridor tiles that the local enumeration misses; this is documented and acceptable as a sanity-check signal, not a coverage proof.

Non-Goals

  • Not covered: producing the RouteSpec — owned by replay_input.tlog_route.extract_route_from_tlog (AZ-836).
  • Not covered: orchestration of when the operator runs the seed — owned by C12 (production binding deferred; cycle-3 e2e fixture operator_pre_flight_setup is the current driver — AZ-839).
  • Not covered: FAISS index construction over the populated cache — owned by C10 DescriptorBatcher.
  • Not covered: bbox-based seeding — handled by tile_downloader.download_tiles_for_area (and by tests/fixtures/derkachi_c6/seed_region.py for the e2e fixture).
  • Not covered: multi-route batching — one RouteSpec per seed_route call. Multi-flight aggregate corridors are an operator-workflow concern.

Versioning Rules

  • Breaking changes (renamed method, removed required field, changed return type, parent-suite Route API contract break) require a major version bump. Coordinate with the C3 fixture (AZ-839) and any future C12 production binding via Choose A/B/C/D before bumping.
  • Non-breaking additions (new optional constructor kwarg, new field on RouteSeedResult, new error variant the consumer catches via SatelliteProviderRouteError) require a minor version bump.
  • The pre-emptive validation bounds (I-1) MUST track the parent-suite CreateRouteRequestValidator.cs exactly. Drift between client and server validators is a defect, not a version concern — fix the client to match the server.

Test Cases

Case Input Expected Notes
route-happy-path RouteSpec for Derkachi tlog (2-waypoint corridor, region_size=500m) against a stubbed satellite-provider returning mapsReady=true on the 2nd poll RouteSeedResult with maps_ready=True, tile_count > 0, terminal_status="completed", elapsed_ms reflects 2 polls AZ-838 AC-1, AC-2
validation-empty-points RouteSpec(waypoints=(), …) RouteValidationError raised BEFORE HTTP POST I-1, AZ-838 AC-6
validation-too-many-points RouteSpec with 501 waypoints RouteValidationError raised BEFORE HTTP POST I-1, AZ-838 AC-6
validation-region-too-large RouteSpec(suggested_region_size_meters=10_001.0, …) RouteValidationError raised BEFORE HTTP POST I-1, AZ-838 AC-6
4xx-problem-details server returns 400 + RFC 7807 errors dict RouteValidationError with field_errors populated from the response I-5, AZ-838 AC-3
5xx-transient server returns 503 RouteTransientError with __cause__ set to the underlying httpx exception I-6, AZ-838 AC-4
terminal-failure server reports status="failed" mid-poll RouteTerminalFailureError; .detail carries the response JSON I-4, AZ-838 AC-5
poll-budget-exhausted server stays in status="processing" past 60 attempts RouteTransientError referencing the last observed status I-3, I-4
inventory-verify-counts-present mapsReady=true then inventory POST returns mixed present=true/false entries tile_count equals the count of present=true entries I-7
integration-derkachi RouteSpec from real Derkachi tlog, against the Jetson satellite-provider (gated by RUN_E2E=1 + SATELLITE_PROVIDER_URL) tile_count > 0, maps_ready=True, completes in ≤ 15 s on the 2-waypoint reference route AZ-838 AC-10 (Jetson-only, Tier-2)

Change Log

Version Date Change Author
1.0.0 2026-05-26 Initial contract — produced by AZ-838 (Epic AZ-835 C2). Cycle-3 addition; consumed by AZ-839 (operator_pre_flight_setup real fixture) and AZ-840 (E2E orchestrator test). autodev