[AZ-284] Autodev baseline + testability refactor

Phase A baseline outputs from /autodev (Steps 1-5):
- Problem & solution docs (_docs/00_problem, _docs/01_solution)
- Codebase documentation (_docs/02_document) incl. architecture,
  module-layout, glossary, system-flows, baseline compliance scan
- Test specs (blackbox, performance, resilience, security, resource,
  traceability matrix)
- Test task decomposition (_docs/02_tasks/todo): AZ-285..AZ-290
- Testability refactor (_docs/04_refactoring/01-testability-refactoring):
  - TC-01 Move DownloadedTileInfoV2 + new ExistingTileInfo to Common.DTO
  - TC-02 Replace dead ISatelliteDownloader API with real signatures
  - TC-03 GoogleMapsDownloaderV2 implements ISatelliteDownloader
  - TC-04 TileService depends on ISatelliteDownloader (mockable)
  - TC-05 DI + endpoints use ISatelliteDownloader
- Test runner scripts (scripts/run-tests.sh, run-performance-tests.sh)
- Autodev state pointer (_docs/_autodev_state.md)

Prepares the codebase for AZ-285..AZ-290 unit/integration test work.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-10 04:44:08 +03:00
parent 25a644a9bf
commit b0fffa6d42
68 changed files with 4192 additions and 11 deletions
+48
View File
@@ -0,0 +1,48 @@
# Acceptance Criteria
## Tile Download
| # | Criterion | Measurable Value | Source |
|---|----------|-----------------|--------|
| T1 | Tiles are cached and not re-downloaded | 0 duplicate downloads for same (lat, lon, zoom, version) | Unique index idx_tiles_unique_location |
| T2 | Concurrent download limit is enforced | Max 4 simultaneous HTTP requests to Google Maps | ProcessingConfig.MaxConcurrentDownloads |
| T3 | Tile stored on disk with correct path | File exists at `./tiles/{zoom}/{x}/{y}.jpg` | TileService storage logic |
| T4 | Tile metadata persisted in database | TileEntity row created with all fields populated | TileRepository.InsertAsync |
## Region Processing
| # | Criterion | Measurable Value | Source |
|---|----------|-----------------|--------|
| R1 | Region transitions through correct states | pending → processing → completed (or failed) | RegionProcessingService state updates |
| R2 | CSV manifest generated on completion | File exists at `./ready/region_{id}_ready.csv` | RegionService.ProcessRegionAsync |
| R3 | Summary file generated on completion | File exists at `./ready/region_{id}_summary.txt` | RegionService.GenerateSummaryFileAsync |
| R4 | Stitched image generated when requested | File exists at `./ready/region_{id}_stitched.jpg` when stitch_tiles=true | RegionService.StitchTilesAsync |
| R5 | Stitched image has valid content | File size > 1024 bytes | Integration test assertion |
| R6 | Region processing is bounded | Max 20 concurrent regions | ProcessingConfig.MaxConcurrentRegions |
## Route Management
| # | Criterion | Measurable Value | Source |
|---|----------|-----------------|--------|
| RT1 | Points interpolated at correct interval | Intermediate points every ~200m along path | RouteService (InterpolatePoints) |
| RT2 | Point types correctly assigned | "original" for input waypoints, "intermediate" for generated | RoutePointEntity.PointType |
| RT3 | Total distance calculated | Haversine sum matches within acceptable precision | RouteService.CreateRoute |
| RT4 | Geofence filtering applied | Only points inside geofence rectangles generate regions | RouteService (point-in-rectangle check) |
| RT5 | ZIP archive within size limit | ≤ 50 MB | RouteProcessingService ZIP generation |
| RT6 | Route map stitched when maps requested | Stitched image > 1024 bytes when request_maps=true | Integration test assertion |
## API Behavior
| # | Criterion | Measurable Value | Source |
|---|----------|-----------------|--------|
| A1 | Region request returns immediately | HTTP 200 with region_id (async processing) | POST /api/satellite/request |
| A2 | Status endpoint reflects real state | Returns current status and file paths | GET /api/satellite/region/{id} |
| A3 | Route creation returns computed metadata | Response includes total_points, total_distance_meters | POST /api/satellite/route |
## System Reliability
| # | Criterion | Measurable Value | Source |
|---|----------|-----------------|--------|
| S1 | Database migrations run on startup | All numbered scripts executed in order | DatabaseMigrator.Migrate() |
| S2 | Queue rejects when full | Channel capacity = 1000, bounded wait | RegionRequestQueue (BoundedChannelOptions) |
| S3 | Failed regions marked as failed | Status = "failed" on unrecoverable error | RegionProcessingService error handling |
+91
View File
@@ -0,0 +1,91 @@
# Data Parameters
## Input Data
### API Request: Single Tile Download
| Parameter | Type | Required | Constraints | Description |
|-----------|------|----------|-------------|-------------|
| latitude | double | yes | -90 to 90 | Center latitude |
| longitude | double | yes | -180 to 180 | Center longitude |
| zoomLevel | int | yes | 120 | Google Maps zoom level |
### API Request: Region
| Parameter | Type | Required | Constraints | Description |
|-----------|------|----------|-------------|-------------|
| latitude | double | yes | -90 to 90 | Region center latitude |
| longitude | double | yes | -180 to 180 | Region center longitude |
| sizeMeters | double | yes | > 0 | Square region side length in meters |
| zoomLevel | int | yes | 120 | Tile zoom level |
| stitchTiles | bool | no | default: false | Whether to produce composite image |
### API Request: Route Creation
| Parameter | Type | Required | Constraints | Description |
|-----------|------|----------|-------------|-------------|
| id | UUID | yes | — | Client-generated route ID |
| name | string | yes | max 200 chars | Human-readable route name |
| description | string | no | — | Optional description |
| regionSizeMeters | double | yes | > 0 | Size of region per route point |
| zoomLevel | int | yes | 120 | Tile zoom level |
| points | array | yes | ≥ 2 waypoints | Ordered route waypoints |
| points[].lat | double | yes | -90 to 90 | Waypoint latitude |
| points[].lon | double | yes | -180 to 180 | Waypoint longitude |
| geofences | object | no | — | Optional geofence definitions |
| geofences.polygons[] | array | no | — | Rectangle boundaries |
| geofences.polygons[].northWest | GeoPoint | yes (if polygon) | valid lat/lon, non-zero | NW corner |
| geofences.polygons[].southEast | GeoPoint | yes (if polygon) | valid lat/lon, non-zero | SE corner |
| requestMaps | bool | no | default: false | Whether to download map tiles for route |
| createTilesZip | bool | no | default: false | Whether to produce ZIP archive |
## Output Data
### Tile File
- **Format**: JPEG
- **Path**: `./tiles/{zoom}/{x}/{y}.jpg`
- **Size**: ~50100 KB per tile (typical at zoom 18)
### Region Outputs
| File | Format | Path Pattern | Content |
|------|--------|-------------|---------|
| CSV manifest | CSV | `./ready/region_{id}_ready.csv` | Tile coordinates and file paths |
| Summary | TXT | `./ready/region_{id}_summary.txt` | Processing statistics |
| Stitched image | JPEG | `./ready/region_{id}_stitched.jpg` | Composite tile image |
### Route Outputs
| File | Format | Path Pattern | Content |
|------|--------|-------------|---------|
| Stitched map | JPEG | `./ready/route_{id}_stitched.jpg` | Full route composite with markers |
| Tiles ZIP | ZIP | `./ready/route_{id}_tiles.zip` | All tiles (max 50 MB) |
| CSV | CSV | `./ready/route_{id}_ready.csv` | Tile manifest |
| Summary | TXT | `./ready/route_{id}_summary.txt` | Route processing statistics |
## Configuration Parameters
### MapConfig (provider-specific; e.g., Google Maps — each provider has its own config section)
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| ApiKey | string | — | Provider authentication token |
| TileSizePixels | int | 256 | Tile image dimension |
| MaxZoomLevel | int | 20 | Maximum allowed zoom |
| DefaultZoomLevel | int | 18 | Default when not specified |
### StorageConfig
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| TilesDirectory | string | "./tiles" | Root tile storage path |
| ReadyDirectory | string | "./ready" | Output artifacts path |
### ProcessingConfig
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| MaxConcurrentDownloads | int | 4 | Parallel Google Maps requests |
| MaxConcurrentRegions | int | 20 | Parallel region processing |
| QueueCapacity | int | 1000 | Max pending region requests |
@@ -0,0 +1,57 @@
# Expected Results Report
## Tile Expected Results
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|----------|----------------|-----------|---------------------|
| TILE-01 | Tile downloaded and stored | — | HTTP 200; response has: zoomLevel=18, tileSizePixels=256, imageType="jpg", filePath non-empty |
| TILE-01 (reuse) | Same tile returned from cache | — | Second request returns same tile ID; no re-download |
## Region Expected Results
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|----------|----------------|-----------|---------------------|
| REG-01 | Region processes to completion | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty |
| REG-02 | Region processes to completion | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty |
| REG-03 | Region with stitching completes | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty; stitched image file > 1024 bytes |
| REG-01 (tile count) | Tiles downloaded > 0 | — | tilesDownloaded + tilesReused > 0 |
## Route Expected Results
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|----------|----------------|-----------|---------------------|
| ROUTE-01 | Route created with interpolated points | — | totalPoints > 2; all intermediate point spacing ≤ 200m; original point count = 2 (first + last) |
| ROUTE-01 (retrieval) | Route retrievable by ID | — | GET /api/satellite/route/{id} returns same route with all points |
| ROUTE-02 | Route maps processed | < 180s | mapsReady=true; stitchedImagePath non-empty; csvFilePath non-empty; stitched image > 1024 bytes |
| ROUTE-03 | Route with ZIP created | < 180s | mapsReady=true; tilesZipPath non-empty; ZIP file > 1024 bytes; ZIP entry count = unique tile count from CSV; ZIP entries start with "tiles/" path prefix; ZIP has directory structure (≥5 path parts) |
| ROUTE-04 | Complex route maps processed | < 240s | mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes |
| ROUTE-05 | Route with geofences processed | < 240s | mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes; geofence regions created |
| ROUTE-06 | Extended route maps processed | < 360s | mapsReady=true; uniqueTileCount ≥ 20; stitched image > 1024 bytes |
## Point Interpolation Expected Results
| Input | Expected | Tolerance | Pass/Fail Criterion |
|-------|----------|-----------|---------------------|
| ROUTE-01 (2 points, ~1.2km apart) | Points interpolated every ~200m | spacing ≤ 200m | Every consecutive point pair has distance ≤ 200m; point_type for first = "original", intermediates = "intermediate", last = "original" |
| ROUTE-04 (10 points) | Original points: first + last = 2; intermediate points = 8 per pair | — | ValidatePointTypes(route, 1, 1, 8) — 1 first-original, 1 last-original, 8 intermediate |
| ROUTE-06 (20 points) | Original points: first + last = 2; intermediate points = 18 | — | ValidatePointTypes(route, 1, 1, 18) |
## API Behavior Expected Results
| Scenario | Expected | Pass/Fail Criterion |
|----------|----------|---------------------|
| Region request (POST) | Immediate response with pending status | HTTP 200; response.status = "pending" or "processing" |
| Region poll (GET) | Status transitions correctly | Eventually reaches "completed" or "failed" |
| Invalid route (< 2 points) | Rejected | HTTP 400 or validation error |
| Tile reuse | Same tile not re-downloaded | tilesReused > 0 when requesting overlapping regions |
## Timing Constraints
| Operation | Max Duration | Source |
|-----------|-------------|--------|
| Single tile download | 30s | HttpClient timeout |
| Region processing (200m, zoom 18) | 240s | Integration test poll limit |
| Route map processing (2 points) | 180s | Integration test poll limit |
| Route map processing (10 points) | 240s | Integration test poll limit |
| Route map processing (20 points) | 360s | Integration test poll limit |
| API readiness on startup | 60s | WaitForApiReady (30 retries × 2s) |
@@ -0,0 +1,95 @@
# Test Coordinates and Input Data
## Geographic Test Region
All test data is centered around eastern Ukraine (Donetsk oblast area):
- Primary tile/region test point: 47.461747°N, 37.647063°E
- Primary route test area: ~48.27°N, 37.38°E
## Tile Inputs
| ID | Latitude | Longitude | Zoom Level |
|----|----------|-----------|------------|
| TILE-01 | 47.461747 | 37.647063 | 18 |
## Region Inputs
| ID | Latitude | Longitude | Size (m) | Zoom | Stitch |
|----|----------|-----------|----------|------|--------|
| REG-01 | 47.461747 | 37.647063 | 200 | 18 | false |
| REG-02 | 47.461747 | 37.647063 | 400 | 17 | false |
| REG-03 | 47.461747 | 37.647063 | 500 | 18 | true |
## Route Inputs
### ROUTE-01: Simple (2 points, no maps)
| Point | Latitude | Longitude |
|-------|----------|-----------|
| 1 (original) | 48.276067180586544 | 37.38445758819581 |
| 2 (original) | 48.27074009522731 | 37.374029159545906 |
Config: regionSizeMeters=500, zoomLevel=18, requestMaps=false
### ROUTE-02: With Region Processing (2 points, maps)
Same points as ROUTE-01.
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
### ROUTE-03: With Tiles ZIP (2 points, maps + zip)
Same points as ROUTE-01.
Config: regionSizeMeters=500, zoomLevel=18, requestMaps=true, createTilesZip=true
### ROUTE-04: Complex (10 points, maps)
| Point | Latitude | Longitude |
|-------|----------|-----------|
| 1 | 48.276067180586544 | 37.38445758819581 |
| 2 | 48.27074009522731 | 37.374029159545906 |
| 3 | 48.263312668696855 | 37.37707614898682 |
| 4 | 48.26539817051818 | 37.36587524414063 |
| 5 | 48.25851283439989 | 37.35952377319337 |
| 6 | 48.254426906081555 | 37.374801635742195 |
| 7 | 48.25914140977405 | 37.39068031311036 |
| 8 | 48.25354110233028 | 37.401752471923835 |
| 9 | 48.25902712391726 | 37.416257858276374 |
| 10 | 48.26828345053738 | 37.402009963989265 |
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
### ROUTE-05: Complex with Geofences (10 points + 2 geofences, maps)
Same 10 points as ROUTE-04.
Geofences:
- Polygon 1: NW(48.280, 37.370) → SE(48.265, 37.395)
- Polygon 2: NW(48.265, 37.390) → SE(48.250, 37.420)
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
### ROUTE-06: Extended (20 points, maps)
| Point | Latitude | Longitude |
|-------|----------|-----------|
| 1 | 48.276067180586544 | 37.51945758819581 |
| 2 | 48.27074009522731 | 37.509029159545906 |
| 3 | 48.263312668696855 | 37.51207614898682 |
| 4 | 48.26539817051818 | 37.50087524414063 |
| 5 | 48.25851283439989 | 37.49452377319337 |
| 6 | 48.254426906081555 | 37.509801635742195 |
| 7 | 48.25914140977405 | 37.52568031311036 |
| 8 | 48.25354110233028 | 37.536752471923835 |
| 9 | 48.25902712391726 | 37.551257858276374 |
| 10 | 48.26828345053738 | 37.537009963989265 |
| 11 | 48.27421563182974 | 37.52345758819581 |
| 12 | 48.26889854647051 | 37.513029159545906 |
| 13 | 48.26147111993905 | 37.51607614898682 |
| 14 | 48.26355662176038 | 37.50487524414063 |
| 15 | 48.25667128564209 | 37.49852377319337 |
| 16 | 48.25258535732375 | 37.513801635742195 |
| 17 | 48.25729986101625 | 37.52968031311036 |
| 18 | 48.25169955357248 | 37.540752471923835 |
| 19 | 48.25718557515946 | 37.555257858276374 |
| 20 | 48.26644190177958 | 37.541009963989265 |
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
+36
View File
@@ -0,0 +1,36 @@
# Problem Statement
## What is the system?
Satellite Provider is a backend service that supplies satellite imagery to a GPS-denied UAV navigation system. It pre-downloads and caches satellite tiles from external imagery providers (Layer 1) and will accept UAV-captured nadir camera imagery (Layer 2) uploaded post-flight. The downloader component is implementation-agnostic — the architecture supports multiple satellite imagery providers (e.g., Google Maps) via the `ISatelliteDownloader` interface.
## What problem does it solve?
UAVs operating without GPS rely on visual navigation by matching real-time camera imagery against pre-existing satellite maps. This requires:
1. **Pre-mission planning**: satellite imagery for the planned route must be downloaded and available before flight, regardless of which imagery provider is used
2. **Post-mission ingestion**: ground truth imagery captured during flight must be stored for future reference and improved accuracy
3. **Tile management**: imagery must be organized by geographic coordinates and zoom level, deduplicated, and served efficiently
4. **Provider flexibility**: the system must not be locked to a single satellite imagery source — providers may change or multiply
Without this service, operators would need to manually download and organize satellite imagery — an error-prone process that doesn't scale to multiple routes or large coverage areas.
## Who are the users?
- **GPS-Denied Navigation Service** — the primary consumer, requesting tiles for route planning and accessing cached imagery for visual positioning
- **Mission Planners** — define routes and regions to pre-download satellite coverage
- **UAV Systems** (planned) — upload nadir camera tiles post-flight for Layer 2 enrichment
## How does it work at a high level?
1. A client defines a geographic area (region) or path (route) via the REST API
2. The service calculates which satellite tiles are needed to cover that area
3. Tiles are downloaded from the configured satellite imagery provider (abstracted via `ISatelliteDownloader` interface; e.g., Google Maps)
4. Tiles are stored on disk and indexed in PostgreSQL
5. Optionally, tiles are stitched into composite images and packaged as ZIP archives
6. The client polls for completion and retrieves output artifacts
For routes, the service additionally:
- Interpolates intermediate points every ~200m along the path
- Applies geofence filters to limit tile downloads to areas of interest
- Generates consolidated route maps with geofence overlays
+40
View File
@@ -0,0 +1,40 @@
# Restrictions
## Software Constraints
| Constraint | Value | Source |
|-----------|-------|--------|
| Runtime | .NET 8.0 (LTS) | global.json, Dockerfile |
| Database | PostgreSQL 16 | docker-compose.yml |
| Container runtime | Docker | Dockerfile, docker-compose.yml |
| Image processing | SixLabors.ImageSharp 3.1.11 | SatelliteProvider.Services.csproj |
| CI platform | Woodpecker CI | .woodpecker/*.yml |
| Target architecture | ARM64 (primary), AMD64 (prepared) | .woodpecker/02-build-push.yml |
## Operational Constraints
| Constraint | Value | Source |
|-----------|-------|--------|
| Deployment model | Single instance (no horizontal scaling) | In-process Channel queue, no distributed state |
| Max concurrent tile downloads | 4 (configurable) | ProcessingConfig |
| Max concurrent region processing | 20 (configurable) | ProcessingConfig |
| Queue capacity | 1000 requests | ProcessingConfig |
| Max ZIP archive size | 50 MB | RouteProcessingService |
| Tile versioning | Year-based integer (e.g., 2025) | Migration 004 |
## Environment Constraints
| Constraint | Value | Source |
|-----------|-------|--------|
| Storage | Local filesystem (./tiles, ./ready, ./logs) | docker-compose.yml volumes |
| Network | Outbound HTTPS to Google Maps required | GoogleMapsDownloaderV2 |
| Authentication | None (internal/trusted network only) | Program.cs (no auth middleware) |
| Database persistence | Docker volume (postgres_data) | docker-compose.yml |
## Dependencies on External Services
| Service | Criticality | Failure Impact |
|---------|-------------|----------------|
| Satellite imagery provider (provider-agnostic via `ISatelliteDownloader`; e.g., Google Maps) | High | No new tiles can be downloaded; cached tiles still served |
| PostgreSQL | Critical | Service cannot start; all state operations fail |
| File system | Critical | Cannot store tiles or produce output artifacts |