mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 08:51:13 +00:00
[AZ-505] Test-spec sync + task-mode doc updates for cycle 6
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:
@@ -14,6 +14,7 @@
|
|||||||
| Layer 1 | Historic name for satellite imagery from external providers (provider-agnostic; first implementation: Google Maps). Generalised in AZ-484 to one of N values of `Tile Source`; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
| Layer 1 | Historic name for satellite imagery from external providers (provider-agnostic; first implementation: Google Maps). Generalised in AZ-484 to one of N values of `Tile Source`; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
||||||
| Layer 2 | Historic name for UAV-captured nadir camera imagery (orthogonal tiles uploaded post-flight). Generalised in AZ-484 to the `uav` `Tile Source` value; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
| Layer 2 | Historic name for UAV-captured nadir camera imagery (orthogonal tiles uploaded post-flight). Generalised in AZ-484 to the `uav` `Tile Source` value; the term is retained for continuity with earlier docs and tickets. | user clarification, AZ-484 |
|
||||||
| Tile Source | The producer of a tile row, persisted in `tiles.source` as a contract-defined string (`google_maps`, `uav`, …). Per AZ-503-foundation: each `(cell, source, flight)` triple may have at most one row; reads return the most-recent across sources AND flights. Adding a new source requires a new `TileSource` enum member and a tile-storage contract version bump. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
| Tile Source | The producer of a tile row, persisted in `tiles.source` as a contract-defined string (`google_maps`, `uav`, …). Per AZ-503-foundation: each `(cell, source, flight)` triple may have at most one row; reads return the most-recent across sources AND flights. Adding a new source requires a new `TileSource` enum member and a tile-storage contract version bump. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
||||||
|
| Tile Inventory | AZ-505 bulk read endpoint (`POST /api/satellite/tiles/inventory`) that returns one metadata entry per requested cell — present/absent + most-recent row's `id`/`capturedAt`/`source`/`flightId`/`resolutionMPerPx` — without streaming any tile bodies. Accepts up to 5000 entries per request in one of two XOR shapes: by-coord (`tiles: [{tileZoom, tileX, tileY}, …]`) or by-hash (`locationHashes: [Guid, …]`). Used by the onboard `gps-denied-onboard` cross-repo path to decide which Google-Maps cells still need download and which UAV variants are already on the server. | _docs/02_document/contracts/api/tile-inventory.md (v1.0.0) |
|
||||||
| Captured At | Producer-defined UTC timestamp ("the moment this tile imagery represents") persisted in `tiles.captured_at`. For Google Maps it is `DateTime.UtcNow` at download time (provider does not expose original imagery date); for UAV it is the capture timestamp supplied by the upload client. Drives the most-recent-across-(source, flight) read selection rule. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
| Captured At | Producer-defined UTC timestamp ("the moment this tile imagery represents") persisted in `tiles.captured_at`. For Google Maps it is `DateTime.UtcNow` at download time (provider does not expose original imagery date); for UAV it is the capture timestamp supplied by the upload client. Drives the most-recent-across-(source, flight) read selection rule. | _docs/02_document/contracts/data-access/tile-storage.md (v2.0.0) |
|
||||||
| UAV Tile Upload | `POST /api/satellite/upload` batch endpoint (AZ-488) that ingests UAV-captured tiles. Multipart envelope with a JSON `metadata` field and an aligned `files` collection; per-item results returned in a single HTTP 200 response. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
| UAV Tile Upload | `POST /api/satellite/upload` batch endpoint (AZ-488) that ingests UAV-captured tiles. Multipart envelope with a JSON `metadata` field and an aligned `files` collection; per-item results returned in a single HTTP 200 response. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
||||||
| Quality Gate | The 5-rule validator (`UavTileQualityGate`) applied to every UAV tile before persistence: Format, Size band, Dimensions, Captured-at age, Blank/uniform. The first failing rule produces a reject reason from the closed `UavTileRejectReasons` enumeration. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
| Quality Gate | The 5-rule validator (`UavTileQualityGate`) applied to every UAV tile before persistence: Format, Size band, Dimensions, Captured-at age, Blank/uniform. The first failing rule produces a reject reason from the closed `UavTileRejectReasons` enumeration. | _docs/02_document/contracts/api/uav-tile-upload.md (v1.0.0) |
|
||||||
|
|||||||
@@ -110,6 +110,33 @@ Authoritative reject-reason codes for the UAV upload quality gate. Adding a new
|
|||||||
- `ImageTooUniform = "IMAGE_TOO_UNIFORM"` — Rule 5 (luminance variance below `MinLuminanceVariance`).
|
- `ImageTooUniform = "IMAGE_TOO_UNIFORM"` — Rule 5 (luminance variance below `MinLuminanceVariance`).
|
||||||
- `StorageFailure = "STORAGE_FAILURE"` — reserved for the orphan-row-recovery path when the on-disk write succeeds but the DB UPSERT fails; surfaced per-item without failing the envelope (AZ-488 Reliability NFR).
|
- `StorageFailure = "STORAGE_FAILURE"` — reserved for the orphan-row-recovery path when the on-disk write succeeds but the DB UPSERT fails; surfaced per-item without failing the envelope (AZ-488 Reliability NFR).
|
||||||
|
|
||||||
|
### TileCoord (added AZ-505)
|
||||||
|
Single tile coordinate triple used by the inventory endpoint Form A request shape and as the per-entry input echo on the response.
|
||||||
|
- `TileZoom` (int) — slippy zoom level.
|
||||||
|
- `TileX`, `TileY` (int) — slippy x/y at that zoom.
|
||||||
|
- Defined in `SatelliteProvider.Common/DTO/TileInventory.cs`. Matches `tile-inventory.md` v1.0.0 Shape.
|
||||||
|
|
||||||
|
### TileInventoryRequest (added AZ-505)
|
||||||
|
API request body for `POST /api/satellite/tiles/inventory`. Carries one of two XOR-exclusive batch shapes.
|
||||||
|
- `Tiles` (`IReadOnlyList<TileCoord>?`) — Form A: coords-by-value. The server computes `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` per entry.
|
||||||
|
- `LocationHashes` (`IReadOnlyList<Guid>?`) — Form B: hashes-by-reference. Used when the caller already has UUIDv5 location hashes (typical for the onboard cross-repo path).
|
||||||
|
- Exactly one of `Tiles` / `LocationHashes` must be populated and non-empty; both-populated or neither → HTTP 400 (`tile-inventory.md` Inv-1).
|
||||||
|
- Total entries (in either field) ≤ `TileInventoryLimits.MaxEntriesPerRequest` (5000); over-cap → HTTP 400 (Inv-7).
|
||||||
|
|
||||||
|
### TileInventoryEntry (added AZ-505)
|
||||||
|
Per-entry result inside `TileInventoryResponse`. One entry per request entry, in the SAME order as the request (`tile-inventory.md` Inv-2).
|
||||||
|
- `LocationHash` (Guid) — always populated; UUIDv5 of `"{z}/{x}/{y}"` from `Uuidv5.LocationHashForTile` (Form A) or echoed from request (Form B).
|
||||||
|
- `Present` (bool) — `true` iff a row exists in `tiles` with this `location_hash` (Inv-4).
|
||||||
|
- `Id` (Guid?) — `tiles.id` of the most-recent row across sources/flights (`captured_at DESC, updated_at DESC, id DESC`, Inv-5); null when `Present=false` (Inv-6).
|
||||||
|
- `CapturedAt` (DateTime?), `Source` (string?), `FlightId` (Guid?), `ResolutionMPerPx` (double?) — populated on the most-recent row; all null when `Present=false`.
|
||||||
|
|
||||||
|
### TileInventoryResponse (added AZ-505)
|
||||||
|
API response body for `POST /api/satellite/tiles/inventory`.
|
||||||
|
- `Results` (`IReadOnlyList<TileInventoryEntry>`) — one entry per request entry; `Results.Count` always equals the request entry count (Inv-2).
|
||||||
|
|
||||||
|
### TileInventoryLimits (added AZ-505, static constants)
|
||||||
|
- `MaxEntriesPerRequest = 5000` — request-body cap enforced by the inventory handler (Inv-7).
|
||||||
|
|
||||||
## Internal Logic
|
## Internal Logic
|
||||||
- `GeoPoint` uses a precision tolerance of `0.00005` degrees (~5.5 meters) for equality comparison.
|
- `GeoPoint` uses a precision tolerance of `0.00005` degrees (~5.5 meters) for equality comparison.
|
||||||
- `SatTile` eagerly computes its bounding box corners on construction by calling `GeoUtils.TileToWorldPos`.
|
- `SatTile` eagerly computes its bounding box corners on construction by calling `GeoUtils.TileToWorldPos`.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Service contracts defining the application's core operations. Implementations li
|
|||||||
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles within a geographic region
|
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles within a geographic region
|
||||||
- `GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes>`: serve a tile by Z/X/Y, hitting cache, then repository, then downloader (added in AZ-310)
|
- `GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes>`: serve a tile by Z/X/Y, hitting cache, then repository, then downloader (added in AZ-310)
|
||||||
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>`: download one tile by lat/lon and persist (added in AZ-311)
|
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>`: download one tile by lat/lon and persist (added in AZ-311)
|
||||||
|
- `GetInventoryAsync(TileInventoryRequest request, CancellationToken) → Task<TileInventoryResponse>`: bulk per-cell metadata read for the `POST /api/satellite/tiles/inventory` endpoint (added AZ-505). Computes `location_hash` per request entry via `Uuidv5.LocationHashForTile` (Form A) or uses the caller-supplied hashes (Form B), delegates the read to `ITileRepository.GetTilesByLocationHashesAsync`, applies the AZ-484 / AZ-503-foundation most-recent-across-sources selection per cell, and shapes the result so `response.results.length == request entry count` in input order (see `tile-inventory.md` v1.0.0 Inv-2..Inv-6). XOR validation + entry-cap enforcement happen in the API handler, not here.
|
||||||
|
|
||||||
### IRegionService
|
### IRegionService
|
||||||
- `RequestRegionAsync(Guid id, double lat, double lon, double sizeMeters, int zoomLevel, bool stitchTiles) → Task<RegionStatusResponse>`: creates a region record and enqueues for async processing
|
- `RequestRegionAsync(Guid id, double lat, double lon, double sizeMeters, int zoomLevel, bool stitchTiles) → Task<RegionStatusResponse>`: creates a region record and enqueues for async processing
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Runs DbUp-based SQL migrations against PostgreSQL on application startup. Ensure
|
|||||||
## Consumers
|
## Consumers
|
||||||
- `Program.cs` — instantiated directly (not via DI) and called during startup. If migration fails, the application throws and does not start.
|
- `Program.cs` — instantiated directly (not via DI) and called during startup. If migration fails, the application throws and does not start.
|
||||||
|
|
||||||
## Migrations (14 scripts)
|
## Migrations (15 scripts)
|
||||||
1. `001_CreateTilesTable.sql`
|
1. `001_CreateTilesTable.sql`
|
||||||
2. `002_CreateRegionsTable.sql`
|
2. `002_CreateRegionsTable.sql`
|
||||||
3. `003_CreateIndexes.sql`
|
3. `003_CreateIndexes.sql`
|
||||||
@@ -38,6 +38,7 @@ Runs DbUp-based SQL migrations against PostgreSQL on application startup. Ensure
|
|||||||
12. `012_DropTileVersionConstraint.sql` — drops the legacy 5-col `(latitude, longitude, tile_zoom, tile_size_meters, version)` unique index, replaces with 4-col `idx_tiles_unique_location` (preparation for AZ-484).
|
12. `012_DropTileVersionConstraint.sql` — drops the legacy 5-col `(latitude, longitude, tile_zoom, tile_size_meters, version)` unique index, replaces with 4-col `idx_tiles_unique_location` (preparation for AZ-484).
|
||||||
13. `013_AddTileSourceAndCapturedAt.sql` — AZ-484 multi-source tile storage. Transactional. Adds `source` (VARCHAR(32) NOT NULL DEFAULT 'google_maps') and `captured_at` (TIMESTAMP NOT NULL) columns; backfills existing rows with `source='google_maps'`, `captured_at=created_at`; drops `idx_tiles_unique_location` and creates 5-col `idx_tiles_unique_location_source` on `(latitude, longitude, tile_zoom, tile_size_meters, source)`. Idempotent against partial replays.
|
13. `013_AddTileSourceAndCapturedAt.sql` — AZ-484 multi-source tile storage. Transactional. Adds `source` (VARCHAR(32) NOT NULL DEFAULT 'google_maps') and `captured_at` (TIMESTAMP NOT NULL) columns; backfills existing rows with `source='google_maps'`, `captured_at=created_at`; drops `idx_tiles_unique_location` and creates 5-col `idx_tiles_unique_location_source` on `(latitude, longitude, tile_zoom, tile_size_meters, source)`. Idempotent against partial replays.
|
||||||
14. `014_AddTileIdentityColumns.sql` — AZ-503 tile-identity foundation. Transactional. Enables the `pgcrypto` extension (`CREATE EXTENSION IF NOT EXISTS pgcrypto`) for the in-migration SHA-1 digest. Adds `flight_id` (UUID NULL), `location_hash` (UUID — backfilled then set NOT NULL), `content_sha256` (BYTEA NULL), `legacy_id` (UUID NULL). Defines a transactional `pg_temp.uuidv5(namespace, name)` PL/pgSQL function that mirrors `SatelliteProvider.Common.Utils.Uuidv5.Create` byte-for-byte, then backfills `location_hash = pg_temp.uuidv5(TILE_NAMESPACE, '{tile_zoom}/{tile_x}/{tile_y}')` and `legacy_id = id` for every pre-existing row. Drops AZ-484's `idx_tiles_unique_location_source` and creates `idx_tiles_unique_identity` UNIQUE on `(tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, '00000000-0000-0000-0000-000000000000'::uuid))` plus a non-unique `idx_tiles_location_hash` on `(location_hash)`. Safe to replay on a partially-migrated database because column adds are `IF NOT EXISTS`-equivalent and `pg_temp.uuidv5` is deterministic — re-running yields the same `location_hash` values.
|
14. `014_AddTileIdentityColumns.sql` — AZ-503 tile-identity foundation. Transactional. Enables the `pgcrypto` extension (`CREATE EXTENSION IF NOT EXISTS pgcrypto`) for the in-migration SHA-1 digest. Adds `flight_id` (UUID NULL), `location_hash` (UUID — backfilled then set NOT NULL), `content_sha256` (BYTEA NULL), `legacy_id` (UUID NULL). Defines a transactional `pg_temp.uuidv5(namespace, name)` PL/pgSQL function that mirrors `SatelliteProvider.Common.Utils.Uuidv5.Create` byte-for-byte, then backfills `location_hash = pg_temp.uuidv5(TILE_NAMESPACE, '{tile_zoom}/{tile_x}/{tile_y}')` and `legacy_id = id` for every pre-existing row. Drops AZ-484's `idx_tiles_unique_location_source` and creates `idx_tiles_unique_identity` UNIQUE on `(tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, '00000000-0000-0000-0000-000000000000'::uuid))` plus a non-unique `idx_tiles_location_hash` on `(location_hash)`. Safe to replay on a partially-migrated database because column adds are `IF NOT EXISTS`-equivalent and `pg_temp.uuidv5` is deterministic — re-running yields the same `location_hash` values.
|
||||||
|
15. `015_AddTilesLeafletPathIndex.sql` — AZ-505 leaflet covering index. Transactional. Creates `tiles_leaflet_path` covering index on `(location_hash, captured_at DESC, updated_at DESC, id DESC) INCLUDE (file_path, source)` so the leaflet hot path (`SELECT file_path FROM tiles WHERE location_hash = $1 ORDER BY captured_at DESC, updated_at DESC, id DESC LIMIT 1`) becomes an `Index Only Scan` once `VACUUM ANALYZE` sets the visibility map. Drops the lightweight `idx_tiles_location_hash` introduced by migration 014 — the new covering index has the same leading column, so equality lookups by `location_hash` use it instead. Lock window: runs in DbUp's per-script transaction (incompatible with `CREATE INDEX CONCURRENTLY`); on a populated `tiles` table the build holds an `ACCESS SHARE` + `SHARE` lock for the build duration, blocking writes (see AZ-505 Risk 2). Inventory queries (`GetTilesByLocationHashesAsync`) intentionally project columns beyond the INCLUDE list (`id`, `captured_at`, `flight_id`, etc.) and therefore trigger a bounded heap fetch — acceptable per AZ-505 NFR-Perf-2 (p95 ≤ 1000 ms / 2500 tiles) and explicit in the migration header.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Receives connection string directly as constructor parameter.
|
Receives connection string directly as constructor parameter.
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ Orchestrates tile downloading and persistence. Bridges the downloader (Google Ma
|
|||||||
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles in a region
|
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles in a region
|
||||||
- `GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes>` (AZ-310): cache → repository → downloader fallback for single Z/X/Y serving
|
- `GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes>` (AZ-310): cache → repository → downloader fallback for single Z/X/Y serving
|
||||||
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata
|
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata
|
||||||
|
- `GetInventoryAsync(TileInventoryRequest request, CancellationToken) → Task<TileInventoryResponse>` (AZ-505): bulk per-cell metadata read for `POST /api/satellite/tiles/inventory`. Steps:
|
||||||
|
1. Project the request to an ordered `Guid[]` of `location_hash` values — either by computing `Uuidv5.LocationHashForTile(z, x, y)` per entry (Form A `request.Tiles`) or by echoing the caller-supplied hashes (Form B `request.LocationHashes`). The request-order vector is retained so step 3 can shape the response in input order.
|
||||||
|
2. Call `ITileRepository.GetTilesByLocationHashesAsync(hashes, CancellationToken)` once. The repository returns the most-recent row per hash (`(captured_at DESC, updated_at DESC, id DESC) LIMIT 1` per `location_hash`); this is the AZ-484 / AZ-503-foundation selection rule preserved at the bulk layer.
|
||||||
|
3. Build a `TileInventoryEntry[]` of the same length as the input vector. For each request slot: emit `Present=false` (only `LocationHash` populated) when no row was returned for that hash; otherwise emit `Present=true` with `Id` / `CapturedAt` / `Source` / `FlightId` / `ResolutionMPerPx` populated from the row. Order matches the request — duplicate hashes in the request produce duplicate entries pointing at the same row (`tile-inventory.md` v1.0.0 Inv-2 / Inv-3).
|
||||||
|
4. XOR validation (both populated / neither populated) and the 5000-entry cap are NOT enforced here — they live in the API handler (`GetTilesInventory` in `Program.cs`) so the HTTP-layer error contract is single-sourced and `ITileService.GetInventoryAsync` can be called from non-HTTP contexts without re-implementing the gate.
|
||||||
|
|
||||||
## Internal Logic
|
## Internal Logic
|
||||||
- New rows write `Version = null` and `MapsVersion = null` (post-AZ-357 / AZ-373); the `version` and `maps_version` columns are retained for backward compatibility with pre-existing rows
|
- New rows write `Version = null` and `MapsVersion = null` (post-AZ-357 / AZ-373); the `version` and `maps_version` columns are retained for backward compatibility with pre-existing rows
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Ripple Log — Cycle 6 (AZ-505)
|
||||||
|
|
||||||
|
## Direct doc updates (Task mode Step 1–4)
|
||||||
|
|
||||||
|
| Doc | Reason |
|
||||||
|
|-----|--------|
|
||||||
|
| `modules/api_program.md` | `Program.cs` — added `POST /api/satellite/tiles/inventory` handler + Kestrel TLS+ALPN config (updated in AC-5 fix commit). |
|
||||||
|
| `modules/common_dtos.md` | `Common/DTO/TileInventory.cs` — 5 new DTOs (`TileCoord`, `TileInventoryRequest`, `TileInventoryEntry`, `TileInventoryResponse`, `TileInventoryLimits`). |
|
||||||
|
| `modules/common_interfaces.md` | `Common/Interfaces/ITileService.cs` — added `GetInventoryAsync`. |
|
||||||
|
| `modules/common_uuidv5.md` | `Common/Utils/Uuidv5.cs` — added `LocationHashForTile(int z, int x, int y)` (consolidated from two prior inline call sites). |
|
||||||
|
| `modules/dataaccess_tile_repository.md` | `DataAccess/Repositories/TileRepository.cs` — `GetByTileCoordinatesAsync` rewired to `location_hash`-keyed predicate; new `GetTilesByLocationHashesAsync` (Npgsql-direct, bypasses Dapper). |
|
||||||
|
| `modules/dataaccess_migrator.md` | New migration 015 (`AddTilesLeafletPathIndex`); migration count bumped 14 → 15. |
|
||||||
|
| `modules/services_tile_service.md` | `Services.TileDownloader/TileService.cs` — `GetInventoryAsync` implementation; `BuildTileEntity` consolidated to `Uuidv5.LocationHashForTile`. |
|
||||||
|
| `modules/dataaccess_models.md` | No structural entity changes, only commentary additions (already updated in initial cycle-6 batch). |
|
||||||
|
| `module-layout.md` | Endpoint + repo-method rows added; Kestrel TLS path noted (updated in AC-5 fix commit). |
|
||||||
|
| `data_model.md` | `tiles.location_hash` column note expanded with AZ-505 usage; `tiles_leaflet_path` index row added; migration 015 row added (already updated in initial cycle-6 batch). |
|
||||||
|
| `system-flows.md` | Added F7 (Leaflet Tile Serving) and F8 (Tile Inventory Bulk Lookup) flows + dependency rows. |
|
||||||
|
| `architecture.md` | Tile-storage contract section already cites AZ-505 + `tile-inventory.md` v1.0.0 (initial cycle-6 batch). No further changes needed. |
|
||||||
|
| `glossary.md` | Added "Tile Inventory" entry; `Location Hash` entry already cites AZ-505 (initial cycle-6 batch). |
|
||||||
|
| `contracts/api/tile-inventory.md` | NEW at v1.0.0 (initial cycle-6 batch); Non-Goals updated for the TLS+ALPN path (AC-5 fix commit). |
|
||||||
|
| `contracts/data-access/tile-storage.md` | Bumped to v2.0.0 (initial cycle-6 batch). |
|
||||||
|
| `tests/blackbox-tests.md` | BT-23..BT-26 appended (Test-Spec Sync, Step 12). |
|
||||||
|
| `tests/performance-tests.md` | PT-09 appended (Test-Spec Sync, Step 12). |
|
||||||
|
| `tests/traceability-matrix.md` | 5 AZ-503 deferred rows resolved + 7 AZ-505 AC rows added + totals + cycle-6 notes (Step 12). |
|
||||||
|
|
||||||
|
## Reverse-dependency ripple (Step 0.5)
|
||||||
|
|
||||||
|
Reverse-dependency analysis for the changed C# source files:
|
||||||
|
|
||||||
|
- `SatelliteProvider.Common/DTO/TileInventory.cs` (new file) — consumed only by `Program.cs` and `TileService.cs`; both already directly updated.
|
||||||
|
- `SatelliteProvider.Common/Interfaces/ITileService.cs` (signature addition `GetInventoryAsync`) — consumed by `Program.cs` (handler wiring); already directly updated.
|
||||||
|
- `SatelliteProvider.Common/Utils/Uuidv5.cs` (added `LocationHashForTile`) — consumed by `TileService.BuildTileEntity` + `TileRepository.GetByTileCoordinatesAsync` + `TileService.GetInventoryAsync`; all already directly updated.
|
||||||
|
- `SatelliteProvider.DataAccess/Repositories/ITileRepository.cs` (signature addition `GetTilesByLocationHashesAsync`) — consumed only by `TileService.GetInventoryAsync`; already directly updated.
|
||||||
|
- `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` (read-path rewrite + bulk add) — consumed via DI through `ITileRepository`; `RegionService` and `RouteService` consume only `GetTilesByRegionAsync` (unchanged), so their docs are NOT stale.
|
||||||
|
- `SatelliteProvider.Services.TileDownloader/TileService.cs` (added `GetInventoryAsync`, consolidated `BuildTileEntity`) — consumed via DI through `ITileService` by `Program.cs` (already updated), `RegionService` (consumes only `DownloadAndStoreTilesAsync` + `GetTilesByRegionAsync`, both unchanged), and `RouteService` (via `RegionService`, no direct dependency).
|
||||||
|
- `SatelliteProvider.Api/Program.cs` — top-level; no upstream consumers in the workspace.
|
||||||
|
- `SatelliteProvider.DataAccess/Migrations/015_AddTilesLeafletPathIndex.sql` — embedded resource; consumed by `DatabaseMigrator` at startup. Migrator doc already updated above.
|
||||||
|
|
||||||
|
**Verdict**: zero stale downstream module docs. The direct-update list above is exhaustive. No directory-proximity heuristic was needed (`csproj` `ProjectReference` graph is small and was walked exhaustively).
|
||||||
|
|
||||||
|
## Tooling note
|
||||||
|
|
||||||
|
This cycle used manual `csproj ProjectReference` traversal (`SatelliteProvider.Api.csproj` → `Common.csproj`, `DataAccess.csproj`, `Services.*` projects) rather than a static analyzer. The dependency graph is shallow (3 levels max) and was walked exhaustively, so the heuristic-fallback mode in the document-skill ripple step was not triggered.
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
| F4 | Route Creation | HTTP POST /api/satellite/route | WebApi, RouteManagement, DataAccess | High |
|
| F4 | Route Creation | HTTP POST /api/satellite/route | WebApi, RouteManagement, DataAccess | High |
|
||||||
| F5 | Route Map Processing | Queue dequeue (background) | RouteManagement, RegionProcessing, TileDownloader, DataAccess | Medium |
|
| 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 |
|
| 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
|
## Flow Dependencies
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@
|
|||||||
| F4 | — | F5 (triggers it) |
|
| F4 | — | F5 (triggers it) |
|
||||||
| F5 | F4 must create route first | F3 (submits region requests) |
|
| F5 | F4 must create route first | F3 (submits region requests) |
|
||||||
| F6 | F2/F4 must exist | — |
|
| 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)
|
DataAccess-->>WebApi: RegionEntity (status, file paths)
|
||||||
WebApi-->>Client: JSON {status, files}
|
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.
|
||||||
|
|||||||
@@ -207,3 +207,40 @@ All Cycle-5 UAV scenarios reuse the AZ-488 envelope. The new observable surface
|
|||||||
**Pass criterion**: All column / index assertions pass AND the deterministic backfill matches the reference function on 100% of sampled rows.
|
**Pass criterion**: All column / index assertions pass AND the deterministic backfill matches the reference function on 100% of sampled rows.
|
||||||
**AC trace**: AZ-503 AC-8.
|
**AC trace**: AZ-503 AC-8.
|
||||||
|
|
||||||
|
## BT-23: Inventory Endpoint — Order Preserved Across Present/Absent Mix
|
||||||
|
|
||||||
|
**Trigger**: `POST /api/satellite/tiles/inventory` with a body of 25 interleaved `(z, x, y)` coords at zoom 18, where 12 of the 25 are seeded into `tiles` (mix of `google_maps` and per-flight `uav` rows) and 13 are absent. Valid JWT attached.
|
||||||
|
**Precondition**: Migration 015 applied (`tiles_leaflet_path` index exists); rows seeded via `TileRepository.AddAsync` so `location_hash` is populated.
|
||||||
|
**Expected**: HTTP 200; response body is a `TileInventoryResponse` with `results.length == 25`; entries appear in the **same order** as the request body; 12 entries have `present=true` with `id` / `locationHash` / `capturedAt` / `source` populated; 13 entries have `present=false` with only `locationHash` populated (UUIDv5 of `"{z}/{x}/{y}"`), `id` is `null`, and `capturedAt` / `source` / `flightId` / `resolutionMPerPx` are all `null`.
|
||||||
|
**Pass criterion**: All ordering, presence, and field-shape invariants from `tile-inventory.md` v1.0.0 Inv-1..Inv-6 hold for every entry.
|
||||||
|
**AC trace**: AZ-505 AC-1 (resolves AZ-503 AC-5 deferral).
|
||||||
|
|
||||||
|
## BT-24: Leaflet Read Path — Most-Recent Selection Keyed on location_hash
|
||||||
|
|
||||||
|
**Trigger**: `GET /tiles/{z}/{x}/{y}` against a cell that has two seeded rows for the same `(z, x, y)`: a `google_maps` row with `captured_at = T - 2h` and a `uav` row with `captured_at = T - 30min` (strictly newer) and a distinct `flight_id`.
|
||||||
|
**Precondition**: Migration 015 applied; both rows persisted with their `location_hash` populated via `Uuidv5.LocationHashForTile`.
|
||||||
|
**Expected**: The DB-level SELECT that `TileRepository.GetByTileCoordinatesAsync` runs (`SELECT … FROM tiles WHERE location_hash = $1 ORDER BY captured_at DESC, updated_at DESC, id DESC LIMIT 1`) returns exactly **one** row — the UAV row.
|
||||||
|
**Pass criterion**: `picked.id == uavId` (the newer row wins); selection semantics are byte-identical to AZ-484 / AZ-503-foundation, only the access key changed from the `(tile_zoom, tile_x, tile_y, tile_size_meters)` 4-tuple to `location_hash`.
|
||||||
|
**AC trace**: AZ-505 AC-2 (resolves AZ-503 AC-6 deferral).
|
||||||
|
|
||||||
|
## BT-25: HTTP/2 Multiplexed Tile Reads on a Single TLS Connection
|
||||||
|
|
||||||
|
**Trigger**: A single `HttpClient` configured with `SocketsHttpHandler { EnableMultipleHttp2Connections = false }` + `HttpVersion.Version20` + `HttpVersionPolicy.RequestVersionExact` issues 20 concurrent `GET /tiles/{z}/{x}/{y}` requests for the same well-known tile.
|
||||||
|
**Precondition**: Kestrel configured with `HttpProtocols.Http1AndHttp2`; dev listener bound to `https://+:8080` with a self-signed cert (`./certs/api.pfx`, generated by `scripts/run-tests.sh`); the integration-test container trusts the cert via `/usr/local/share/ca-certificates/` + `update-ca-certificates`. ALPN advertises `h2` and `http/1.1`.
|
||||||
|
**Expected**: All 20 responses succeed with HTTP 200; each `HttpResponseMessage.Version` equals `2.0`; per-tile `ETag` and `Cache-Control` headers are preserved unchanged from the HTTP/1.1 baseline; all 20 streams share a single TLS connection (enforced by `EnableMultipleHttp2Connections = false`).
|
||||||
|
**Pass criterion**: 20/20 responses are `Status=200`, `Version=2.0`, with non-null `ETag` and non-null `Cache-Control`.
|
||||||
|
**Note (post-merge correction)**: Original AZ-505 wording was "dev plaintext endpoint" / h2c; switched to TLS+ALPN during the cycle-6 Run-Tests step because Kestrel silently downgrades `Http1AndHttp2` to HTTP/1.1 over plaintext (ALPN cannot run unencrypted). See `_docs/03_implementation/implementation_report_tile_inventory_cycle6.md` → "Post-merge correction".
|
||||||
|
**AC trace**: AZ-505 AC-5 (resolves AZ-503 AC-12 deferral).
|
||||||
|
|
||||||
|
## BT-26: Inventory Endpoint — Request Validation
|
||||||
|
|
||||||
|
**Trigger**: Four `POST /api/satellite/tiles/inventory` calls, each exercising one validation rule.
|
||||||
|
**Precondition**: API up; valid JWT attached except for the anonymous case.
|
||||||
|
**Expected**:
|
||||||
|
- Both `tiles` and `locationHashes` populated → HTTP 400 with a descriptive `detail` per `tile-inventory.md` Inv-1.
|
||||||
|
- Neither `tiles` nor `locationHashes` populated → HTTP 400.
|
||||||
|
- `tiles.length > 5000` or `locationHashes.length > 5000` → HTTP 400 (the 5000 cap matches `TileInventoryLimits.MaxEntriesPerRequest`, Inv-7).
|
||||||
|
- No `Authorization: Bearer …` header → HTTP 401 before the handler runs (matches the `.RequireAuthorization()` baseline; same shape as SEC-05).
|
||||||
|
**Pass criterion**: All four expected status codes returned; no response leaks server internals.
|
||||||
|
**AC trace**: AZ-505 AC-6.
|
||||||
|
|
||||||
|
|||||||
@@ -62,3 +62,14 @@
|
|||||||
**Expected**: Per-item quality-gate cost target < 50 ms (Rule 5 dominates — luminance variance after the 32×32 downsample). End-to-end p95 for a 10-item batch < 2 s on the dev hardware (8-core x86 baseline; revise on hardware change).
|
**Expected**: Per-item quality-gate cost target < 50 ms (Rule 5 dominates — luminance variance after the 32×32 downsample). End-to-end p95 for a 10-item batch < 2 s on the dev hardware (8-core x86 baseline; revise on hardware change).
|
||||||
**Pass criterion**: `p95(UploadUavTileBatch[10 items]) ≤ 2000ms`. The harness reports `batch_p50`, `batch_p95`, and a `per_item_proxy_p95 = batch_p95 / batch_size` derived value plus accepted/rejected/failed counts. The 2000 ms threshold gates batch p95; per-item gate cost is a derived proxy (precise per-call `UavTileQualityGate.Validate` timing requires server-side instrumentation that is out of scope for AZ-492 — see `_docs/06_metrics/perf_<date>.md` for the recorded numbers and follow-up items).
|
**Pass criterion**: `p95(UploadUavTileBatch[10 items]) ≤ 2000ms`. The harness reports `batch_p50`, `batch_p95`, and a `per_item_proxy_p95 = batch_p95 / batch_size` derived value plus accepted/rejected/failed counts. The 2000 ms threshold gates batch p95; per-item gate cost is a derived proxy (precise per-call `UavTileQualityGate.Validate` timing requires server-side instrumentation that is out of scope for AZ-492 — see `_docs/06_metrics/perf_<date>.md` for the recorded numbers and follow-up items).
|
||||||
**Source**: AZ-488 NFR (Performance) — `_docs/02_tasks/done/AZ-488_uav_tile_upload.md` § Non-Functional Requirements; harness landed in AZ-492.
|
**Source**: AZ-488 NFR (Performance) — `_docs/02_tasks/done/AZ-488_uav_tile_upload.md` § Non-Functional Requirements; harness landed in AZ-492.
|
||||||
|
|
||||||
|
## PT-09: Inventory Endpoint Throughput (2500-Tile Batch)
|
||||||
|
|
||||||
|
**Status**: **Implemented (AZ-505).** Embedded in the integration test `TileInventoryTests.PerformanceBudget_AC4` (full-suite only; smoke run prints a documented skip). Not yet promoted to `scripts/run-performance-tests.sh` because the AZ-505 gate is met inline; promotion is a candidate follow-up if the budget tightens (see Note below).
|
||||||
|
|
||||||
|
**Trigger**: `POST /api/satellite/tiles/inventory` with a 2500-entry `tiles` body at zoom 18 against a populated DB. The test seeds 2500 `(z, x, y)` cells (one `google_maps` row each) inside a single transaction, runs `VACUUM ANALYZE tiles`, then issues 20 identical inventory requests serially and records per-call wall-clock latency.
|
||||||
|
**Load**: 20 calls × 2500-tile batches (single client, sequential — the bottleneck under test is server-side query planning + array binding + per-row hash lookup, not network concurrency).
|
||||||
|
**Expected**: p95 over the 20 measured calls ≤ 1000 ms. The plan is expected to consume the `tiles_leaflet_path` covering index on the leading `location_hash` column (with `Index Only Scan` for cells where the visibility map is complete, falling back to a bounded heap fetch otherwise — `tile-inventory.md` v1.0.0 documents this trade-off explicitly).
|
||||||
|
**Pass criterion**: `p95(durations[20]) ≤ 1000ms` (samples sorted ascending; p95 = `sorted[18]` over 20 samples per the test). Cycle 6 measured: `min=13ms, median=19ms, p95=66ms, max=117ms` — well under budget.
|
||||||
|
**Source**: AZ-505 AC-4 — `_docs/02_tasks/done/AZ-505_tile_inventory_http2_leaflet_index.md` § Acceptance Criteria. Resolves the AZ-503 AC-9 perf NFR deferral (budget relaxed from 500 ms to 1000 ms during AZ-505 scoping — see AZ-505 Risk 1 in the task spec).
|
||||||
|
**Note (promotion to perf harness)**: The in-test gate runs against the same Docker compose stack as the rest of the integration suite, so the perf budget is verified on the same hardware as functional tests. If we ever need to tighten the budget (e.g., to 500 ms for production-equivalent hardware) or add cross-commit baseline comparison like PT-07, promote this to `scripts/run-performance-tests.sh § PT-09` with a `PERF_INVENTORY_BATCH_SIZE` env variable controlling the row count and a separate cold/warm distinction.
|
||||||
|
|||||||
@@ -93,11 +93,18 @@
|
|||||||
| AZ-503 AC-7 | content_sha256 is computed and persisted; byte-identical bodies produce identical digest | BT-20 (blackbox); `UavTileUploadHandlerTests.HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (unit) | ✓ |
|
| AZ-503 AC-7 | content_sha256 is computed and persisted; byte-identical bodies produce identical digest | BT-20 (blackbox); `UavTileUploadHandlerTests.HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (unit) | ✓ |
|
||||||
| AZ-503 AC-8 | Migration 014 adds columns + supersedes AZ-484 index + backfills location_hash deterministically | BT-22 (blackbox); `MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull`, `Az503NewUniqueIndexCoversIntegerKeyAndFlightId`, `Az503LocationHashBackfillIsDeterministic`, `Az503MigrationSupersedesAz484UniqueIndex` (integration) | ✓ |
|
| AZ-503 AC-8 | Migration 014 adds columns + supersedes AZ-484 index + backfills location_hash deterministically | BT-22 (blackbox); `MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull`, `Az503NewUniqueIndexCoversIntegerKeyAndFlightId`, `Az503LocationHashBackfillIsDeterministic`, `Az503MigrationSupersedesAz484UniqueIndex` (integration) | ✓ |
|
||||||
| AZ-503 AC-11 | Per-flight on-disk separation (`./tiles/uav/{flight_id\|none}/{z}/{x}/{y}.jpg`) | BT-19 (blackbox); `UavTileFilePathTests.BuildUavTileFilePath_AnonymousFlight_UsesNoneSegment`, `_PerFlight_UsesFlightIdDirectory`, `_DifferentFlights_ProduceDifferentPaths` (unit); `UavUploadTests.MultiFlightUavRowsCoexist_AZ503_AC3` (integration; per-flight file_path assertion) | ✓ |
|
| AZ-503 AC-11 | Per-flight on-disk separation (`./tiles/uav/{flight_id\|none}/{z}/{x}/{y}.jpg`) | BT-19 (blackbox); `UavTileFilePathTests.BuildUavTileFilePath_AnonymousFlight_UsesNoneSegment`, `_PerFlight_UsesFlightIdDirectory`, `_DifferentFlights_ProduceDifferentPaths` (unit); `UavUploadTests.MultiFlightUavRowsCoexist_AZ503_AC3` (integration; per-flight file_path assertion) | ✓ |
|
||||||
| AZ-503 AC-5 | Inventory endpoint `POST /api/satellite/tiles/inventory` returns one entry per requested coord | — | ◐ deferred → AZ-505 |
|
| AZ-503 AC-5 | Inventory endpoint `POST /api/satellite/tiles/inventory` returns one entry per requested coord | BT-23 (blackbox); resolved by AZ-505 AC-1 — see row below | ✓ (via AZ-505) |
|
||||||
| AZ-503 AC-6 | Leaflet path returns most-recent variant via `location_hash` | — | ◐ deferred → AZ-505 |
|
| AZ-503 AC-6 | Leaflet path returns most-recent variant via `location_hash` | BT-24 (blackbox); resolved by AZ-505 AC-2 — see row below | ✓ (via AZ-505) |
|
||||||
| AZ-503 AC-9 | Inventory endpoint p95 ≤ 500 ms for 2500 tiles | — | ◐ deferred → AZ-505 (perf NFR) |
|
| AZ-503 AC-9 | Inventory endpoint p95 ≤ 500 ms for 2500 tiles | PT-09 (performance); resolved by AZ-505 AC-4 — see row below (budget relaxed to 1000 ms under AZ-505 scoping, AZ-505 Risk 1) | ✓ (via AZ-505, relaxed budget) |
|
||||||
| AZ-503 AC-10 | Leaflet hot path is index-only (EXPLAIN: no heap fetch when `voting_status='trusted'`) | — | ◐ deferred → AZ-505 |
|
| AZ-503 AC-10 | Leaflet hot path is index-only (EXPLAIN: no heap fetch when `voting_status='trusted'`) | Resolved by AZ-505 AC-3 — see row below (voting layer is deferred to a future task per AZ-505 Non-Goals; AC-3 verifies the index-only access path against `tiles_leaflet_path` directly via `EXPLAIN ANALYZE` + `Heap Fetches: 0` assertion) | ✓ (via AZ-505, voting-filter deferred) |
|
||||||
| AZ-503 AC-12 | HTTP/2 multiplexed responses for `/tiles/{z}/{x}/{y}` | — | ◐ deferred → AZ-505 |
|
| AZ-503 AC-12 | HTTP/2 multiplexed responses for `/tiles/{z}/{x}/{y}` | BT-25 (blackbox); resolved by AZ-505 AC-5 — see row below | ✓ (via AZ-505) |
|
||||||
|
| AZ-505 AC-1 | Inventory endpoint returns one entry per requested coord in input order; present/absent shaping per `tile-inventory.md` Inv-1..Inv-6 | BT-23 (blackbox); `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` (integration) | ✓ |
|
||||||
|
| AZ-505 AC-2 | Leaflet read path returns most-recent variant keyed on `location_hash` (`captured_at DESC, updated_at DESC, id DESC LIMIT 1`) | BT-24 (blackbox); `TileInventoryTests.LeafletReadReturnsMostRecentViaLocationHash_AC2` (integration; DB-level verification of the exact SELECT used by `TileRepository.GetByTileCoordinatesAsync`, which `ServeTile` wraps unchanged) | ✓ |
|
||||||
|
| AZ-505 AC-3 | Leaflet hot path uses `Index Only Scan using tiles_leaflet_path`; `Heap Fetches` ≤ 1 after `VACUUM ANALYZE`; query time < 1 ms | `LeafletPathIndexOnlyTests.RunAll` (integration; `EXPLAIN ANALYZE` + regex + `Heap Fetches ≤ 1`; smoke run falls back to `SET enable_seqscan = off` if the optimiser hasn't picked the index naturally — measures index *capability*, not optimiser heuristic) | ✓ |
|
||||||
|
| AZ-505 AC-4 | Inventory endpoint p95 ≤ 1000 ms for 2500 tiles over 20 calls | PT-09 (performance); `TileInventoryTests.PerformanceBudget_AC4` (integration; full-suite only, smoke prints a documented skip). Cycle 6 measured: `p95=66ms, max=117ms` — well under budget. | ✓ |
|
||||||
|
| AZ-505 AC-5 | HTTP/2 multiplexed responses for `/tiles/{z}/{x}/{y}` over a single connection with preserved ETag + Cache-Control headers | BT-25 (blackbox); `Http2MultiplexingTests.RunAll` (integration; 20 concurrent GETs over a single TLS connection with `SocketsHttpHandler { EnableMultipleHttp2Connections = false }` + `HttpVersion.Version20` + `RequestVersionExact`). Implementation uses TLS+ALPN on the dev `https://+:8080` listener (cert generated by `scripts/run-tests.sh` into `./certs/api.pfx`, trusted in the integration-tests container via `update-ca-certificates`) — the original h2c plan was switched mid-cycle because Kestrel silently downgrades `Http1AndHttp2` to HTTP/1.1 over plaintext (no ALPN). See `_docs/03_implementation/implementation_report_tile_inventory_cycle6.md` → "Post-merge correction". | ✓ |
|
||||||
|
| AZ-505 AC-6 | Request validation — 400 on both populated, 400 on neither, 400 on > 5000 entries, 401 on anonymous | BT-26 (blackbox); `TileInventoryTests.ValidationRejectsBothPopulated_AC6`, `ValidationRejectsNeitherPopulated_AC6`, `ValidationRejectsOversizedBatch_AC6`, `UnauthenticatedRequestReturns401_AC6` (integration) | ✓ |
|
||||||
|
| AZ-505 AC-7 | Contract artifacts produced in the same commit as code (`tile-inventory.md` v1.0.0, `tile-storage.md` v2.0.0 Change Log, `module-layout.md` rows) | Doc inspection at completeness gate — `_docs/03_implementation/implementation_completeness_cycle6_report.md` "Files / Symbols Checked" + "Contracts" sections list the v1.0.0 / v2.0.0 artifacts; no runtime test (deliberately doc-only) | ✓ (doc-only) |
|
||||||
| AZ-504 AC-1 | PT-08 completes on zero-rejected response (no script exit under `set -e -o pipefail`) | Standalone shell harness (4-case) executed in batch_01_cycle5_report.md — accepted/rejected counters wrapped in `{ grep -o … \|\| true; }` at `scripts/run-performance-tests.sh:416-417`; structural: `rg "grep -o .* \\\| wc -l" scripts/run-performance-tests.sh` returns 0 unguarded sites | ✓ |
|
| AZ-504 AC-1 | PT-08 completes on zero-rejected response (no script exit under `set -e -o pipefail`) | Standalone shell harness (4-case) executed in batch_01_cycle5_report.md — accepted/rejected counters wrapped in `{ grep -o … \|\| true; }` at `scripts/run-performance-tests.sh:416-417`; structural: `rg "grep -o .* \\\| wc -l" scripts/run-performance-tests.sh` returns 0 unguarded sites | ✓ |
|
||||||
| AZ-504 AC-2 | PT-08 completes on zero-accepted response (defensive) | Same standalone shell harness (case 4) — `accepted=0, rejected=N` path no longer kills the script | ✓ |
|
| AZ-504 AC-2 | PT-08 completes on zero-accepted response (defensive) | Same standalone shell harness (case 4) — `accepted=0, rejected=N` path no longer kills the script | ✓ |
|
||||||
| AZ-504 AC-3 | PT-08 summary line prints in full default-parameter perf run | Verified at autodev Step 15 (Performance Test) by running `scripts/run-performance-tests.sh` with `PERF_REPEAT_COUNT=20 PERF_UAV_BATCH_SIZE=10`; pass criterion is the `PT-08 UAV batch upload: PASS p95=Xms / 2000ms (...)` line in the run output | ◐ gate at Step 15 |
|
| AZ-504 AC-3 | PT-08 summary line prints in full default-parameter perf run | Verified at autodev Step 15 (Performance Test) by running `scripts/run-performance-tests.sh` with `PERF_REPEAT_COUNT=20 PERF_UAV_BATCH_SIZE=10`; pass criterion is the `PT-08 UAV batch upload: PASS p95=Xms / 2000ms (...)` line in the run output | ◐ gate at Step 15 |
|
||||||
@@ -148,9 +155,10 @@
|
|||||||
| Cycle 1 — AZ-484 (integration + unit) | 6 | 7/7 | — |
|
| Cycle 1 — AZ-484 (integration + unit) | 6 | 7/7 | — |
|
||||||
| Cycle 2 — AZ-487 (integration + unit + behavioral) | 4 integration + 3 unit + 1 behavioral | 8/8 | — |
|
| Cycle 2 — AZ-487 (integration + unit + behavioral) | 4 integration + 3 unit + 1 behavioral | 8/8 | — |
|
||||||
| Cycle 2 — AZ-488 (integration + unit + blackbox) | 7 integration + 14 unit + 6 blackbox | 10/10 | — |
|
| Cycle 2 — AZ-488 (integration + unit + blackbox) | 7 integration + 14 unit + 6 blackbox | 10/10 | — |
|
||||||
| Cycle 5 — AZ-503 foundation (integration + unit + blackbox) | 2 integration + 6 unit + 4 blackbox | 7/12 in-scope (AC-1, 2, 3, 4, 7, 8, 11); 5 ACs deferred → AZ-505 | — |
|
| Cycle 5 — AZ-503 foundation (integration + unit + blackbox) | 2 integration + 6 unit + 4 blackbox | 7/12 in-scope (AC-1, 2, 3, 4, 7, 8, 11); 5 ACs deferred → AZ-505 (now resolved in cycle 6) | — |
|
||||||
| Cycle 5 — AZ-504 perf-script fix (shell harness + Step-15 gate) | 1 standalone shell harness (4 cases) | 2/4 verified now (AC-1, AC-2); 2/4 gated at Step 15 (AC-3, AC-4) | — |
|
| Cycle 5 — AZ-504 perf-script fix (shell harness + Step-15 gate) | 1 standalone shell harness (4 cases) | 2/4 verified now (AC-1, AC-2); 2/4 gated at Step 15 (AC-3, AC-4) | — |
|
||||||
| **Total** | **90** | **56/56 in-scope (100%); 5 explicitly deferred to AZ-505 next cycle; 2 AZ-504 ACs gated at Step 15** | **8/8 (100%)** |
|
| Cycle 6 — AZ-505 inventory + HTTP/2 + leaflet covering index (integration + blackbox + perf) | 3 integration files + 4 blackbox (BT-23..BT-26) + 1 perf (PT-09) | 7/7 (AC-1..AC-7; AC-7 is doc-only). Also resolves the 5 AZ-503 deferrals (AC-5, 6, 9, 10, 12). | — |
|
||||||
|
| **Total** | **94** | **63/63 in-scope (100%); 2 AZ-504 ACs gated at Step 15** | **8/8 (100%)** |
|
||||||
|
|
||||||
**Coverage shape notes (Cycle 5 — AZ-503 foundation):**
|
**Coverage shape notes (Cycle 5 — AZ-503 foundation):**
|
||||||
- AZ-503 was split mid-cycle (Option C, autodev Step 10 batch 2): 7 of 12 original ACs land here; 5 (AC-5, AC-6, AC-9, AC-10, AC-12) are deferred to AZ-505 with a `Blocks` link in Jira and an entry in `_docs/02_tasks/_dependencies_table.md`. The deferred rows above are marked `◐ deferred → AZ-505` so the matrix surfaces the scope boundary explicitly.
|
- AZ-503 was split mid-cycle (Option C, autodev Step 10 batch 2): 7 of 12 original ACs land here; 5 (AC-5, AC-6, AC-9, AC-10, AC-12) are deferred to AZ-505 with a `Blocks` link in Jira and an entry in `_docs/02_tasks/_dependencies_table.md`. The deferred rows above are marked `◐ deferred → AZ-505` so the matrix surfaces the scope boundary explicitly.
|
||||||
@@ -170,3 +178,11 @@
|
|||||||
- AZ-500 AC-5 (perf-script bootstrap) demoted the cycle-3 SDK-mismatch leftover to a script-bug leftover (PT-08 grep-pipefail at `scripts/run-performance-tests.sh:417`). The full PT-01..PT-08 perf gate moves to cycle 4 Step 15 (Performance Test). The PT-07 / PT-08 coverage rows above remain `✓` because they reflect the harness's *measurement capability*, not the per-cycle measurement run.
|
- AZ-500 AC-5 (perf-script bootstrap) demoted the cycle-3 SDK-mismatch leftover to a script-bug leftover (PT-08 grep-pipefail at `scripts/run-performance-tests.sh:417`). The full PT-01..PT-08 perf gate moves to cycle 4 Step 15 (Performance Test). The PT-07 / PT-08 coverage rows above remain `✓` because they reflect the harness's *measurement capability*, not the per-cycle measurement run.
|
||||||
- AZ-500 NFRs (Compatibility / Performance / Reliability / Security) propagate to existing rows rather than introducing new gates: Compatibility ⇒ cycle-3 architecture-compliance baseline (verified by Step 11 suite); Performance ⇒ Step 15 perf gate (PT-07/PT-08); Reliability ⇒ no `dotnet restore` failures in the migrated state (Step 11 build path); Security ⇒ Step 14 dependency-scan re-run.
|
- AZ-500 NFRs (Compatibility / Performance / Reliability / Security) propagate to existing rows rather than introducing new gates: Compatibility ⇒ cycle-3 architecture-compliance baseline (verified by Step 11 suite); Performance ⇒ Step 15 perf gate (PT-07/PT-08); Reliability ⇒ no `dotnet restore` failures in the migrated state (Step 11 build path); Security ⇒ Step 14 dependency-scan re-run.
|
||||||
- Restriction "**.NET 8.0 runtime**" was rewritten to "**.NET 10 runtime**" — this is a supersession (toolchain bump) not a new gate, so no Choose was needed per cycle-update rule 3.
|
- Restriction "**.NET 8.0 runtime**" was rewritten to "**.NET 10 runtime**" — this is a supersession (toolchain bump) not a new gate, so no Choose was needed per cycle-update rule 3.
|
||||||
|
|
||||||
|
**Coverage shape notes (Cycle 6 — AZ-505 inventory + HTTP/2 + leaflet covering index):**
|
||||||
|
- AZ-505 resolves the five AZ-503 deferrals (AC-5, AC-6, AC-9, AC-10, AC-12) and adds two strictly new ACs (AC-6 request validation, AC-7 contract-artifacts-in-same-commit). The deferred AZ-503 rows above are rewritten from `◐ deferred → AZ-505` to `✓ (via AZ-505)` and now point at the cycle-6 test entries — the AZ-503 contract is preserved, the implementation just landed one cycle later.
|
||||||
|
- AZ-503 AC-9's original 500 ms p95 budget for 2500 tiles was relaxed to 1000 ms during AZ-505 scoping (AZ-505 Risk 1 documents the trade-off: the inventory result set projects columns beyond `tiles_leaflet_path`'s INCLUDE list, so a bounded heap fetch is unavoidable). The cycle-6 measured p95 is `66 ms` — 15× under the relaxed budget — so the relaxation is conservative, not load-bearing.
|
||||||
|
- AZ-503 AC-10 originally specified `Heap Fetches: 0 when voting_status='trusted'`. The voting layer is deferred to a future task per `tile-inventory.md` v1.0.0 Non-Goals (`voting / trust-promotion filtering`). AZ-505 AC-3 verifies the index-only access path against `tiles_leaflet_path` directly via `EXPLAIN ANALYZE` + `Heap Fetches ≤ 1` assertion, which is the AC-10 intent minus the voting filter. When voting lands, AC-10's `voting_status='trusted'` predicate will be re-verified by the voting task.
|
||||||
|
- AZ-505 AC-5 originally specified h2c (HTTP/2 over plaintext). Kestrel was switched to TLS+ALPN on `https://+:8080` during the cycle-6 Run Tests step because `HttpProtocols.Http1AndHttp2` silently downgrades to HTTP/1.1 over plaintext (no ALPN). The functional gate (multiplexing semantics) is unchanged — the test still asserts `HttpResponseMessage.Version == 2.0` over 20 concurrent GETs on a single connection. The deployment caveat (dev cert vs. production TLS termination at the ingress) is documented in `tile-inventory.md` Non-Goals.
|
||||||
|
- AZ-505 NFRs propagate as follows: Performance (AC-3, AC-4) ⇒ PT-09 entry (full PT-09 row in `performance-tests.md`); Compatibility (existing `GET /tiles/{z}/{x}/{y}` byte-identical) ⇒ no new test — the AZ-484 / AZ-503-foundation selection rule is unchanged, and the test that exercised it under the old `(z, x, y)`-keyed SELECT now exercises it under the `location_hash`-keyed SELECT via AC-2; Security (JWT + `RequireAuthorization()`) ⇒ AC-6 anonymous-401 case, BT-26.
|
||||||
|
- Cycle-update rule check: no NFR conflicts surfaced. The 500 ms → 1000 ms perf budget relaxation between AZ-503 AC-9 and AZ-505 AC-4 is **not** a conflict in the cycle-update sense — AZ-503 AC-9 was explicitly deferred (`◐ deferred → AZ-505`) so AZ-505 owns the binding budget; AZ-503's number was a pre-implementation estimate. The matrix records both numbers and the rationale so the budget history stays auditable.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## Current Step
|
## Current Step
|
||||||
flow: existing-code
|
flow: existing-code
|
||||||
step: 12
|
step: 14
|
||||||
name: Test-Spec Sync
|
name: Security Audit
|
||||||
status: not_started
|
status: not_started
|
||||||
sub_step:
|
sub_step:
|
||||||
phase: 0
|
phase: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user