[AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-15 sync (test-spec / docs / security / perf)

Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule
validation surface and 12 cycle-7 AC rows + Coverage Summary
update to traceability-matrix.md.

Step 13 (Update Docs): module-layout + module docs for the new
SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler
+ updated TileInventory DTO; tests_unit + tests_integration
document the new InventoryRequestValidatorTests (16 unit tests
covering all 9 rules) + TileInventoryValidationTests (16
integration tests) + ProblemDetailsAssertions support;
glossary entries for Validation Problem Details / FluentValidation
/ Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk
Lookup) expanded with deserializer + validator gates and a 13-row
Validation Surface table; data_parameters § Tile Inventory
documents the v2 input schema + constraints; ripple_log_cycle7
captures the doc-side ripple decisions.

Step 14 (Security Audit): 5-phase audit ran; verdict
PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation
12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message
leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak).
No Critical / High; auth runs before validation (confirmed in
Program.cs); two NuGet additions (FluentValidation 12.0.0 +
.DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase
reports plus consolidated security_report_cycle7.md.

Step 15 (Performance Test): docker compose stack used for perf
run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios
PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke
probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring
min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4
1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS
handshake-overhead identification, not application-side change.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-22 11:24:27 +03:00
parent 865dfdb3b9
commit bc04ba7f99
17 changed files with 779 additions and 32 deletions
+20 -8
View File
@@ -327,11 +327,11 @@ sequenceDiagram
---
## Flow F8: Tile Inventory Bulk Lookup (added AZ-505)
## Flow F8: Tile Inventory Bulk Lookup (added AZ-505; renamed + strict-validated AZ-794+AZ-795+AZ-796, cycle 7)
### 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.
Programmatic clients (httpx `http2=True`, .NET `HttpClient`, onboard cross-repo callers) post a batch of up to 5000 `{z, x, y}` triples (Form A; the wire field names were renamed from `tileZoom/tileX/tileY` by AZ-794, cycle 7) 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
@@ -345,8 +345,9 @@ sequenceDiagram
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)
Note over Kestrel,GetTilesInventory: AZ-795 deserializer guards (UnmappedMemberHandling.Disallow + [JsonRequired])<br/>catch unknown / missing / type-mismatched fields → 400 ValidationProblemDetails<br/>via GlobalExceptionHandler (cycle 7)
GetTilesInventory->>GetTilesInventory: ValidationEndpointFilter<TileInventoryRequest><br/>(InventoryRequestValidator — AZ-796 cycle 7)
Note over GetTilesInventory: 9 business rules: XOR / non-empty / per-array cap / Z range / X+Y range
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)
@@ -357,15 +358,26 @@ sequenceDiagram
GetTilesInventory-->>Client: 200 OK, JSON (results in input order)
```
### Validation Surface
### Validation Surface (post-cycle 7 — AZ-795 + AZ-796)
| 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) |
| Empty / missing body | System.Text.Json (`[JsonRequired]` on `Tiles`/`LocationHashes` covered indirectly via `InventoryRequestValidator`) | 400 + `ValidationProblemDetails` |
| Both `tiles` and `locationHashes` populated | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR — `tile-inventory.md` v2.0.0 Inv-1) | 400 + `ValidationProblemDetails`, key `""` |
| Neither populated | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR) | 400 + `ValidationProblemDetails`, key `""` |
| `tiles` or `locationHashes` array is empty | `InventoryRequestValidator` `.Must(...)` (Rule 1, XOR — non-empty arm) | 400 + `ValidationProblemDetails`, key `""` |
| `tiles.Count > 5000` (`TileInventoryLimits.MaxEntriesPerRequest`) | `InventoryRequestValidator` `.Must(t => t.Count <= 5000)` (Rule 6 — Inv-7) | 400 + `ValidationProblemDetails`, key `tiles` |
| `locationHashes.Count > 5000` | `InventoryRequestValidator` `.Must(h => h.Count <= 5000)` (Rule 7 — Inv-7) | 400 + `ValidationProblemDetails`, key `locationHashes` |
| `tiles[i].z` missing OR out of range (must be `0..22` inclusive) | `[JsonRequired]` on `Z` (deserializer) + `TileCoordValidator` Rule 4 | 400 + `ValidationProblemDetails`, key `tiles[i].z` |
| `tiles[i].x` missing OR `< 0` OR `>= 2^z` | `[JsonRequired]` on `X` (deserializer) + `TileCoordValidator` Rule 5 | 400 + `ValidationProblemDetails`, key `tiles[i].x` |
| `tiles[i].y` missing OR `< 0` OR `>= 2^z` | `[JsonRequired]` on `Y` (deserializer) + `TileCoordValidator` Rule 5 | 400 + `ValidationProblemDetails`, key `tiles[i].y` |
| Legacy `tileZoom/tileX/tileY` field names | `UnmappedMemberHandling.Disallow` (deserializer; AZ-794 + AZ-795) | 400 + `ValidationProblemDetails`, key `tiles[i].tileZoom` (etc.) |
| Unknown root or nested field | `UnmappedMemberHandling.Disallow` (deserializer; AZ-795) | 400 + `ValidationProblemDetails`, key on the unknown path |
| Wrong JSON type (e.g. `"z": "18"`) | System.Text.Json type-mismatch (deserializer; AZ-795) | 400 + `ValidationProblemDetails`, key on the offending path |
| No `Authorization: Bearer …` header | `.RequireAuthorization()` | 401 before handler runs |
All 4xx bodies conform to `error-shape.md` v1.0.0. The same `ValidationProblemDetails` shape is emitted whether the failure was caught by the FluentValidation business-rule layer (`InventoryRequestValidator`) or by the deserializer layer (via `GlobalExceptionHandler`). Both layers are unit + integration tested in `SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests` and `SatelliteProvider.IntegrationTests/TileInventoryValidationTests`.
### 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.