[AZ-505] Test-spec sync + task-mode doc updates for cycle 6
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful

Step 12 (Test-Spec Sync, cycle-update mode):
- blackbox-tests.md: append BT-23..BT-26 for AZ-505's new
  observable behaviors (inventory order/shape; leaflet
  most-recent via location_hash; HTTP/2 multiplex over TLS+ALPN;
  request validation).
- performance-tests.md: append PT-09 (inventory p95 ≤ 1000ms /
  2500 tiles); records cycle-6 measured p95=66ms; documents
  promotion path to scripts/run-performance-tests.sh if budget
  ever tightens.
- traceability-matrix.md: resolve the 5 AZ-503 deferrals
  (AC-5/6/9/10/12) by pointing at AZ-505 test names + add 7
  AZ-505 AC rows (AC-1..AC-7) + bump totals (90 -> 94 tests,
  56/56 -> 63/63 in-scope) + add cycle-6 coverage shape notes
  (budget relaxation rationale, voting-filter deferral note,
  TLS+ALPN pivot, NFR propagation).

Step 13 (Update Docs, task mode):
- common_dtos.md: add 5 new TileInventory DTOs.
- common_interfaces.md: add ITileService.GetInventoryAsync.
- services_tile_service.md: document TileService.GetInventoryAsync
  steps + the XOR-validation-in-handler note.
- dataaccess_migrator.md: bump migration count 14 -> 15;
  describe migration 015 (AZ-505 leaflet covering index, lock
  window, INCLUDE-list trade-off).
- system-flows.md: add F7 (Leaflet Tile Serving, AZ-310 +
  AZ-505 location_hash rewire + TLS+ALPN) and F8 (Tile
  Inventory Bulk Lookup) with sequence diagrams, validation
  surface, and AC-4 perf evidence. Update Flow Inventory +
  Dependencies tables accordingly.
- glossary.md: add "Tile Inventory" entry pointing at the
  v1.0.0 contract.
- ripple_log_cycle6.md: new file — exhaustive reverse-dependency
  analysis confirms zero stale downstream module docs.

Advance autodev state from step 11 -> 14 (skipping 12+13 as
completed in this commit; auto-chain through Step 14 = Security
Audit optional gate).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 22:29:22 +03:00
parent c74a2339aa
commit 5d84d2839e
11 changed files with 246 additions and 10 deletions
+94
View File
@@ -10,6 +10,8 @@
| F4 | Route Creation | HTTP POST /api/satellite/route | WebApi, RouteManagement, DataAccess | High |
| F5 | Route Map Processing | Queue dequeue (background) | RouteManagement, RegionProcessing, TileDownloader, DataAccess | Medium |
| F6 | Status Query | HTTP GET /api/satellite/region/{id} or /route/{id} | WebApi, DataAccess | Low |
| F7 | Leaflet Tile Serving | HTTP GET /tiles/{z}/{x}/{y} | WebApi, TileService, DataAccess, FileSystem | High |
| F8 | Tile Inventory Bulk Lookup | HTTP POST /api/satellite/tiles/inventory | WebApi, TileService, DataAccess | High |
## Flow Dependencies
@@ -21,6 +23,8 @@
| F4 | — | F5 (triggers it) |
| F5 | F4 must create route first | F3 (submits region requests) |
| F6 | F2/F4 must exist | — |
| F7 | F1 or F3 must have populated the tile (else 404) | F1, F3, F8 (shares `tiles.location_hash`) |
| F8 | — | F1, F3, F7 (shares `tiles.location_hash`) |
---
@@ -275,3 +279,93 @@ sequenceDiagram
DataAccess-->>WebApi: RegionEntity (status, file paths)
WebApi-->>Client: JSON {status, files}
```
---
## Flow F7: Leaflet Tile Serving (added AZ-310, rewired AZ-505)
### Description
Leaflet (or any HTTP/2-capable client) requests a single tile body by slippy `(z, x, y)`. The handler resolves the most-recent variant across sources/flights by `location_hash` and streams the JPEG body back. AZ-505 rewired the lookup predicate from `(tile_zoom, tile_x, tile_y)` to `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` so the read hits the `tiles_leaflet_path` covering index as an `Index Only Scan`; the selection rule (`captured_at DESC, updated_at DESC, id DESC LIMIT 1`) is unchanged from AZ-484 / AZ-503-foundation. Kestrel runs `Http1AndHttp2` over TLS (`https://+:8080` in dev) so ALPN multiplexes many concurrent leaflet requests on a single TLS connection.
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant Kestrel
participant ServeTile
participant TileService
participant TileRepo
participant FileSystem
Client->>Kestrel: GET /tiles/{z}/{x}/{y} (HTTP/2 via TLS+ALPN)
Kestrel->>ServeTile: route match
ServeTile->>TileService: GetOrDownloadTileAsync(z, x, y)
TileService->>TileRepo: GetByTileCoordinatesAsync(z, x, y)
Note over TileRepo: WHERE location_hash = $1<br/>ORDER BY captured_at DESC, updated_at DESC, id DESC<br/>LIMIT 1<br/>Index Only Scan: tiles_leaflet_path
alt Cached
TileRepo-->>TileService: TileEntity
TileService->>FileSystem: read file_path
FileSystem-->>TileService: JPEG bytes
TileService-->>ServeTile: TileBytes (file_path, ETag, Cache-Control)
ServeTile-->>Client: 200 OK, JPEG body
else Miss
TileService->>TileService: download via GoogleMapsDownloaderV2
Note over TileService: persists row + on-disk path, falls through to ServeTile
TileService-->>ServeTile: TileBytes
ServeTile-->>Client: 200 OK, JPEG body
end
```
### Error Handling
| Failure | Detection | Handling |
|---------|-----------|----------|
| Tile not present and downloader rejects (404 from Google Maps) | `TileService.GetOrDownloadTileAsync` propagates the downloader's `HttpRequestException` | Returns 500; `ServeTile` does NOT translate this to 404 because the predicate matched (path-traversal cases below 404 earlier in routing) |
| Path traversal in `/tiles/...` segment | ASP.NET Core route binding | `400`/`404` before the handler runs (covered by SEC-02) |
---
## Flow F8: Tile Inventory Bulk Lookup (added AZ-505)
### Description
Programmatic clients (httpx `http2=True`, .NET `HttpClient`, onboard cross-repo callers) post a batch of up to 5000 `(z, x, y)` triples (Form A) or up to 5000 pre-computed `location_hash` UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's `id`, `capturedAt`, `source`, `flightId`, and `resolutionMPerPx`. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard `gps-denied-onboard` workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server.
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant Kestrel
participant GetTilesInventory
participant TileService
participant TileRepo
Client->>Kestrel: POST /api/satellite/tiles/inventory (JWT, Form A or B)
Kestrel->>GetTilesInventory: route match
GetTilesInventory->>GetTilesInventory: XOR check (both/neither populated → 400)
GetTilesInventory->>GetTilesInventory: cap check (count > 5000 → 400)
GetTilesInventory->>TileService: GetInventoryAsync(request)
Note over TileService: Form A: compute location_hash per coord<br/>via Uuidv5.LocationHashForTile<br/>Form B: echo caller-supplied hashes
TileService->>TileRepo: GetTilesByLocationHashesAsync(hashes)
Note over TileRepo: NpgsqlCommand:<br/>SELECT DISTINCT ON (location_hash) ...<br/>WHERE location_hash = ANY($1::uuid[])<br/>ORDER BY location_hash, captured_at DESC, updated_at DESC, id DESC<br/>(bypasses Dapper IEnumerable expansion)
TileRepo-->>TileService: IReadOnlyDictionary<Guid, TileEntity>
TileService->>TileService: shape into TileInventoryEntry[] in request order
TileService-->>GetTilesInventory: TileInventoryResponse
GetTilesInventory-->>Client: 200 OK, JSON (results in input order)
```
### Validation Surface
| Input | Detection | Response |
|-------|-----------|----------|
| Both `tiles` and `locationHashes` populated | Handler XOR check | 400 + ProblemDetails (`tile-inventory.md` Inv-1) |
| Neither populated | Handler XOR check | 400 + ProblemDetails |
| `count > 5000` (`TileInventoryLimits.MaxEntriesPerRequest`) | Handler cap check | 400 + ProblemDetails (Inv-7) |
| No `Authorization: Bearer …` header | `.RequireAuthorization()` | 401 before handler runs |
### Performance
p95 ≤ 1000 ms for 2500-coord batches (AZ-505 AC-4). Cycle-6 measured: p95=66ms — well under budget. The covering index (`tiles_leaflet_path`) supplies the leading `location_hash` lookup; the projection's columns beyond the INCLUDE list (`id`, `captured_at`, `flight_id`, ...) trigger a bounded heap fetch which is documented and accepted per the AZ-505 NFR.