diff --git a/_docs/02_tasks/_dependencies_table.md b/_docs/02_tasks/_dependencies_table.md index 52f2e37..1b6451b 100644 --- a/_docs/02_tasks/_dependencies_table.md +++ b/_docs/02_tasks/_dependencies_table.md @@ -102,7 +102,15 @@ Source: cross-workspace handoff from `gps-denied-onboard` (tile-schema scenario |------|-------|-----------|--------|--------| | AZ-503 | Tile identity → UUIDv5 + integer UPSERT (foundation half — split from original AZ-503) | AZ-484 (supersedes UPSERT-conflict-key portion of AZ-484 selection rule) | 3 | Done (In Testing, batch 2 cycle 5) | | AZ-504 | Perf script: fix grep \| wc -l pipefail crash in PT-08 | — (independent; references AZ-488 PT-08 threshold) | 1 | Done (In Testing, batch 1 cycle 5) | -| AZ-505 | Tile inventory endpoint + HTTP/2 + leaflet covering index | AZ-503 (HARD, Blocks-linked — needs `location_hash` + `flight_id` columns) | 3 | To Do (cycle 6 candidate) | +| AZ-505 | Tile inventory endpoint + HTTP/2 + leaflet covering index | AZ-503 (HARD, Blocks-linked — needs `location_hash` + `flight_id` columns) | 3 | To Do (consumed by cycle 6 — see below) | + +### Step 9 cycle 6 — New Task: Tile inventory endpoint + HTTP/2 + Leaflet covering index (AZ-483 epic) + +Source: cycle-5 retro Action 2 — AZ-505 is the deferred half of AZ-503 (inventory endpoint + HTTP/2 + Leaflet covering index). AZ-503-foundation (cycle 5) shipped the prerequisite columns (`location_hash`, `flight_id`, `content_sha256`, `legacy_id`); AZ-505 ships the user-facing payload that consumes them. + +| Task | Title | Depends On | Points | Status | +|------|-------|-----------|--------|--------| +| AZ-505 | Tile inventory endpoint + HTTP/2 + leaflet covering index | AZ-503 (HARD, Blocks-linked, satisfied by cycle 5) | 3 | To Do (cycle 6) | ## Execution Order @@ -153,7 +161,13 @@ Independent tracks — both can run in parallel; no ordering constraint between 1. AZ-504 (1 SP) — cheapest unblocker; lands first to clear PT-08 reporting for the cycle. 2. AZ-503 (3 SP, foundation half) — main feature; data-model + identity plumbing; cross-workspace alignment with `gps-denied-onboard` AZ-304. -3. AZ-505 (3 SP) — deferred to next cycle; `Blocks`-linked to AZ-503. +3. AZ-505 (3 SP) — deferred to cycle 6; `Blocks`-linked to AZ-503. + +### Step 9 cycle 6 + +Single task; consumes the AZ-503-foundation columns landed in cycle 5. + +1. AZ-505 (3 SP) — Tile inventory endpoint + HTTP/2 + Leaflet covering index. Self-contained but produces TWO contract artifacts (new `contracts/api/tile-inventory.md` v1.0.0 + bump `contracts/data-access/tile-storage.md` v1.0.0 → v2.0.0 per architecture.md). ## Total Effort @@ -165,6 +179,7 @@ Step 9 cycle 2: 2 tasks created (AZ-487 = 2 pts, AZ-488 = 8 pts over-cap user-ac Step 9 cycle 3: 6 tasks created (AZ-491 = 3 pts, AZ-492 = 3 pts, AZ-493 = 2 pts, AZ-494 = 2 pts, AZ-495 = 1 pt, AZ-496 = 2 pts) — total 13 pts Step 9 cycle 4: 1 task created (AZ-500 = 5 pts) Step 9 cycle 5: 3 tasks tracked (AZ-503 = 3 pts foundation-half, AZ-504 = 1 pt, AZ-505 = 3 pts split-off-deferred) — 4 pts committed to cycle 5, 3 pts deferred to cycle 6 +Step 9 cycle 6: 1 task scheduled (AZ-505 = 3 pts) — consumed from cycle-5 deferral ## Coverage Verification diff --git a/_docs/02_tasks/todo/AZ-505_tile_inventory_http2_leaflet_index.md b/_docs/02_tasks/todo/AZ-505_tile_inventory_http2_leaflet_index.md new file mode 100644 index 0000000..8795e72 --- /dev/null +++ b/_docs/02_tasks/todo/AZ-505_tile_inventory_http2_leaflet_index.md @@ -0,0 +1,179 @@ +# Tile inventory endpoint + HTTP/2 + Leaflet covering index + +**Task**: AZ-505_tile_inventory_http2_leaflet_index +**Name**: Tile inventory endpoint + HTTP/2 + leaflet covering index +**Description**: Ship the user-facing payload that justifies the AZ-503-foundation schema work — new `POST /api/satellite/tiles/inventory` for batched existence/metadata lookup, `tiles_leaflet_path` covering index that makes `GET /tiles/{z}/{x}/{y}` an index-only scan, and Kestrel HTTP/2 enablement so consumers can multiplex tile reads on one TCP connection. +**Complexity**: 3 points +**Dependencies**: AZ-503 (HARD, Jira `Blocks`-linked — needs `location_hash` + `flight_id` columns from migration 014; landed cycle 5) +**Component**: SatelliteProvider.Api + SatelliteProvider.DataAccess + SatelliteProvider.Services.TileDownloader + SatelliteProvider.Common +**Tracker**: AZ-505 +**Epic**: AZ-483 — Multi-source tile storage + UAV upload (Layer 2) + +## Origin + +Split out of AZ-503 (cycle 5) during /autodev Step 10 batch 2 via Option C scope-protection. AZ-503-foundation shipped the deterministic identity + integer UPSERT + `flight_id` / `location_hash` / `content_sha256` columns. AZ-505 ships the consumer-facing endpoints + covering index that consume those columns. See `_docs/02_tasks/done/AZ-503_tile_identity_uuidv5_bulk_list.md` § "Scope split note (cycle 5 /autodev Step 10 batch 2)" and `_docs/06_metrics/retro_2026-05-12_cycle5.md` § Action 2 for the split rationale. Jira AZ-505 spec is the authoritative description; this file mirrors it with in-workspace-only sections (codebase insertion points, contract obligations, test-coverage gap analysis, scope-discipline carve-outs). + +## Problem + +After AZ-503-foundation lands, three follow-on capabilities remain unimplemented (verbatim from Jira AZ-505): + +1. **No bulk-list endpoint** — onboard `TileDownloader` (`gps-denied-onboard` AZ-316) calls `POST /api/satellite/tiles/inventory` for pre-flight cache sizing. Endpoint does not exist; closest is single-tile `GET /api/satellite/tiles/latlon` or private `GetTilesByRegionAsync`. Operators cannot pre-size a cache build over the mission planner's bbox today. +2. **No Leaflet covering index** — migration 014 (AZ-503-foundation) added `location_hash` + a lightweight `idx_tiles_location_hash` lookup index, but explicitly leaves the `tiles_leaflet_path` covering index for AZ-505. `GET /tiles/{z}/{x}/{y}` still hits the heap on every read because the current `GetByTileCoordinatesAsync` filters by `(tile_zoom, tile_x, tile_y)` and the covering index does not exist yet. +3. **HTTP/1.1 only on plaintext endpoint** — Kestrel defaults to HTTP/1.1 + HTTP/2 over HTTPS only. The onboard side configures `httpx.Client(http2=True)` but cannot multiplex over the dev plaintext endpoint; Leaflet browser opens up to 6 TCP connections instead of multiplexing 30+ tile streams over one. + +## Outcome + +- **New endpoint** `POST /api/satellite/tiles/inventory`: body accepts EITHER `{ "tiles": [{z,x,y}] }` OR `{ "locationHashes": [uuid] }` (never both — 400 if both populated, 400 if neither). Response is one entry per input in the SAME order as input. Per-tile fields: `tile_x`, `tile_y`, `tile_zoom`, `location_hash`, `present` (bool); when `present=true`: `id`, `captured_at`, `source`, `flight_id` (nullable), `resolution_m_per_px` (derived from `tile_size_meters` / `tile_size_pixels`). `estimated_bytes` is **excluded** — per Jira spec, nullable + null-until-profiling-justifies-stat-cost, deferred. +- **Request cap**: max 5000 entries per inventory call (2× headroom over the AC-4 perf gate of 2500). Anything larger returns HTTP 400. +- Server-side query (single round-trip, indexed): `SELECT DISTINCT ON (location_hash) ... FROM tiles WHERE location_hash = ANY($1::uuid[]) ORDER BY location_hash, captured_at DESC, updated_at DESC, id DESC`. Most-recent-across-sources rule applies, identical semantics to existing `GetByTileCoordinatesAsync`. Voting filter is **not** applied — voting is a separate task. +- **Covering index** `CREATE INDEX tiles_leaflet_path ON tiles (location_hash, captured_at DESC, updated_at DESC, id DESC) INCLUDE (file_path, source)`. The lightweight `idx_tiles_location_hash` from migration 014 is dropped (superseded — equality lookup uses the leading column of the covering index). Leaflet `GET /tiles/{z}/{x}/{y}` is rewritten in `TileService` / `TileRepository.GetByTileCoordinatesAsync` to compute `location_hash` upfront and filter on it; target plan is `Index Only Scan using tiles_leaflet_path` with `Heap Fetches = 0`. +- **HTTP/2 enabled** in Kestrel: `builder.WebHost.ConfigureKestrel(opts => opts.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2))`. HTTP/3 / QUIC is out of scope (ALPN + UDP plumbing not verified through dev compose). +- **Contract artifacts** (Step 4.5 obligations): + - New `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0 produced in this task (template: `.cursor/skills/decompose/templates/api-contract.md`). + - `_docs/02_document/contracts/data-access/tile-storage.md` v1.0.0 → **v2.0.0 major** bump (architecture.md already names AZ-505 as owner: "v2.0.0 bump tracking the AZ-503 identity columns is deferred to AZ-505 when the new identity surface freezes for external consumers"). The bump captures: `flight_id`, `location_hash`, `content_sha256`, `legacy_id` columns; `idx_tiles_unique_identity` integer UPSERT key; `tiles_leaflet_path` covering index; new `location_hash`-keyed read selection rule on `GetByTileCoordinatesAsync`. + - `_docs/02_document/module-layout.md` — new endpoint row added under `modules/api_program.md` Public Interface; new repo method added under `dataaccess_tile_repository.md` Public API. + +## Scope + +### Included + +- `SatelliteProvider.Common/DTO/TileInventory.cs` — new `TileInventoryRequest`, `TileInventoryResponse`, `TileInventoryEntry`, `TileCoord` records. +- `SatelliteProvider.DataAccess/Repositories/ITileRepository.cs` + `TileRepository.cs` — new `GetTilesByLocationHashesAsync(IReadOnlyList locationHashes)` method; rewrite `GetByTileCoordinatesAsync` to compute `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` and filter by it (semantically identical results to current implementation; same most-recent-across-sources tie-break preserved). +- `SatelliteProvider.Services.TileDownloader/TileService.cs` (+ `ITileService` interface in `SatelliteProvider.Common.Interfaces`) — new `GetInventoryAsync(...)` method that owns the request → location_hash mapping, repo call, and response shaping. +- `SatelliteProvider.Api/Program.cs` — new `app.MapPost("/api/satellite/tiles/inventory", ...).RequireAuthorization()` with `.WithOpenApi(...)` matching existing endpoint style; new `builder.WebHost.ConfigureKestrel(...)` call enabling `HttpProtocols.Http1AndHttp2`. +- `SatelliteProvider.DataAccess/Migrations/015_AddTilesLeafletPathIndex.sql` — `CREATE INDEX tiles_leaflet_path ... INCLUDE (file_path, source)` + `DROP INDEX IF EXISTS idx_tiles_location_hash`. +- `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0 (new). +- `_docs/02_document/contracts/data-access/tile-storage.md` v1.0.0 → v2.0.0 (major bump, change log entry, listed consumers reviewed). +- `_docs/02_document/module-layout.md` — endpoint row + repo method row. +- `_docs/02_document/glossary.md` — drop the "Reserved for the AZ-505 Leaflet covering index" qualifier on "Location Hash"; promote to "Drives the Leaflet covering index `tiles_leaflet_path` and the `POST /api/satellite/tiles/inventory` endpoint". +- `SatelliteProvider.IntegrationTests/TileInventoryTests.cs` (new) — covers AC-1, AC-2, AC-4 (perf), and the 5000-entry cap + 400-on-both-bodies / 400-on-neither edge cases. +- `SatelliteProvider.IntegrationTests/Http2MultiplexingTests.cs` (new) — covers AC-5 using `HttpClient { DefaultRequestVersion = HttpVersion.Version20, DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact }`. The 20 concurrent GETs must complete and report `HttpResponseMessage.Version == 2.0` for all. +- `SatelliteProvider.IntegrationTests/LeafletPathIndexOnlyTests.cs` (new) — covers AC-3 by executing `EXPLAIN (ANALYZE, BUFFERS) SELECT file_path FROM tiles WHERE location_hash = $1 ORDER BY ...` and asserting the plan contains `Index Only Scan using tiles_leaflet_path` and `Heap Fetches:` ≤ a small number (visibility map state on freshly-loaded rows is environment-dependent; per Jira AC-3 the assertion is "Heap Fetches = 0 ... visibility-map fully built" with the practical relaxation that the test seeds enough rows + runs `VACUUM ANALYZE tiles` before measuring). + +### Excluded + +- Tile identity foundation (UUIDv5, `flight_id`, `content_sha256`, `location_hash` column + backfill, integer-keyed UPSERT) — owned by AZ-503-foundation (done cycle 5). This task assumes it has landed. +- Voting / trust-promotion layer — gps-denied-onboard Design Task #2; consumes `flight_id`; not consumed here. No `voting_status` filter on the inventory query. +- HTTP/3 / QUIC end-to-end — defer pending dev-compose ALPN/UDP verification. +- PMTiles / multipart / tar / zip bundle endpoint — rejected by AZ-503 parent spec rationale (HTTP/2 multistream is sufficient). +- `estimated_bytes` field on the inventory response — Jira spec defers (per-file `stat` cost not justified until production profiling). +- nginx `http2 on;` directive — no nginx in current dev compose stack; production TLS termination is a deployment-layer concern. +- Cycle-4 carry-overs that are NOT AZ-505 scope per `coderule.mdc` scope-discipline (re-listed here so review does not silently fold them in): `Microsoft.NET.Test.Sdk` 17.8.0 transitive `NuGet.Frameworks` advisory (D2-cy4); `Microsoft.IdentityModel.Tokens` / `System.IdentityModel.Tokens.Jwt` 7.0.3 → 7.1.2+ bump (D-IdentityModel-7.0.3); `WithOpenApi(...)` `ASPDEPR002` migration to ASP.NET Core 10 minimal-API metadata extensions (cycle-4 Action 2); `Serilog.AspNetCore` 10.x recheck. Each of these is a separate PBI candidate. + +## Acceptance Criteria + +**AC-1: Inventory endpoint returns one entry per requested coord, in input order** +Given a POST body of 25 `(z, x, y)` coords at zoom 18, 12 already present in DB (mix of `google_maps` and per-flight `uav` rows) and 13 absent +When `POST /api/satellite/tiles/inventory` is called with valid JWT +Then `results` contains 25 entries in the SAME ORDER as input; 12 entries have `present=true` with `id`/`location_hash`/`captured_at`/`source` populated; 13 entries have `present=false` with `location_hash` populated (computed via UUIDv5) and `id=null`. + +**AC-2: Leaflet path returns most-recent variant via location_hash** +Given multiple rows exist for the same `(z, x, y)` cell from different sources/flights with distinct `captured_at` values +When `GET /tiles/{z}/{x}/{y}` is called +Then exactly ONE tile body is returned, selected by `WHERE location_hash = $1 ORDER BY captured_at DESC, updated_at DESC, id DESC LIMIT 1`; the result is semantically identical to the current AZ-484 / AZ-503-foundation selection rule but now keyed on `location_hash`. + +**AC-3: Leaflet hot path uses the covering index** +Given the `tiles_leaflet_path` covering index exists and the `tiles` table holds ≥ 100k rows with `VACUUM ANALYZE` having run +When `EXPLAIN (ANALYZE, BUFFERS) SELECT file_path FROM tiles WHERE location_hash = $1 ORDER BY captured_at DESC, updated_at DESC, id DESC LIMIT 1` is run +Then the plan contains `Index Only Scan using tiles_leaflet_path`; `Heap Fetches:` is 0 (or, if the test environment cannot guarantee visibility-map completeness, ≤ 1); total execution time is < 1 ms. + +**AC-4: Inventory endpoint performance — ≤ 1000 ms p95 for 2500 tiles** +Given a POST body listing 2500 `(z, x, y)` coords at zoom 18 against a populated DB (~3 versions per cell averaged across `google_maps` + `uav` sources) +When `POST /api/satellite/tiles/inventory` is called repeatedly (20 calls) +Then the p95 response time is ≤ 1000 ms; the expected query plan involves an index scan over `tiles_leaflet_path` (verifiable via `EXPLAIN`). + +**AC-5: HTTP/2 multiplexed responses** +Given Kestrel is configured with `HttpProtocols.Http1AndHttp2` on the dev plaintext endpoint +When a single `HttpClient` configured for `HttpVersion.Version20` + `HttpVersionPolicy.RequestVersionExact` issues 20 concurrent `GET /tiles/{z}/{x}/{y}` requests +Then all 20 responses succeed; each `HttpResponseMessage.Version == 2.0`; per-tile `ETag` + `Cache-Control` headers are preserved unchanged from the HTTP/1.1 baseline. + +**AC-6: Request validation — body shape, cap, JWT** +- Given a POST body that populates BOTH `tiles` AND `locationHashes`, the endpoint returns HTTP 400 with a descriptive `detail`. +- Given a POST body that populates NEITHER, the endpoint returns HTTP 400 with a descriptive `detail`. +- Given a POST body with > 5000 entries (either `tiles` or `locationHashes`), the endpoint returns HTTP 400. +- Given no Bearer token, the endpoint returns HTTP 401 before reaching the handler (matches existing `.RequireAuthorization()` baseline). + +**AC-7: Contract artifacts produced in the same commit as the code** +- A new file `_docs/02_document/contracts/api/tile-inventory.md` exists at v1.0.0 with all required sections from `decompose/templates/api-contract.md`. +- `_docs/02_document/contracts/data-access/tile-storage.md` is bumped to v2.0.0 with a Change Log entry naming this task, listing the four AZ-503-foundation columns + `tiles_leaflet_path` index + new `location_hash`-keyed read rule as the breaking-but-additive changes. +- `_docs/02_document/module-layout.md` is updated with the new endpoint + repo method rows. + +## Non-Functional Requirements + +**Performance** +- AC-3: Leaflet path is index-only-scan against `tiles_leaflet_path`; total query time < 1 ms with `VACUUM ANALYZE`-current state. +- AC-4: `POST /api/satellite/tiles/inventory` p95 ≤ 1000 ms for 2500 tiles. +- New PT-09 (inventory p95) candidate scenario for `_docs/02_document/tests/performance-tests.md` — added during Step 12 Test-Spec Sync, not in this task's deliverables. + +**Compatibility** +- Existing `GET /tiles/{z}/{x}/{y}` behavior must be byte-identical for callers: same JPEG returned, same `Cache-Control` + `ETag` headers. The internal query path changes from `(tile_zoom, tile_x, tile_y)` to `location_hash`-keyed but the result is the same row. +- HTTP/1.1 continues to be accepted (Kestrel `Http1AndHttp2` keeps H1 on). +- Existing OpenAPI clients regenerated against the new spec see the inventory endpoint as additive; no existing endpoint shape changes. + +**Reliability** +- Migration 015 must run online (covering index creation on a populated table). Use `CREATE INDEX CONCURRENTLY` if the table size warrants it AND DbUp supports the non-transactional execution path — otherwise document the lock acquisition window in the migration comments. Investigate during implementation; do not block the spec on this. + +## Unit Tests + +| AC Ref | What to Test | Required Outcome | +|--------|--------------|-------------------| +| AC-1 | `TileService.GetInventoryAsync` mapping logic (request → location_hash list → repo call → response ordering) | Returns entries in input order; present-vs-absent shaping is correct given a stub repo | +| AC-6 | `TileInventoryRequest` validation (both-populated / neither-populated / > 5000) | Validation returns expected reject reason per branch | + +## Blackbox Tests + +| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References | +|--------|------------------------|--------------|-------------------|----------------| +| AC-1 | DB seeded with 12 mixed-source rows at known `(z, x, y)` cells; 13 cells empty | POST inventory with all 25 coords in interleaved order | Response preserves order; 12 present, 13 absent; per-entry fields populated correctly | — | +| AC-2 | DB seeded with 2 rows for same `(z, x, y)`: `google_maps captured_at=T1`, `uav captured_at=T2 > T1` | GET `/tiles/{z}/{x}/{y}` | Returns the UAV tile body (most-recent rule preserved across the rewrite) | — | +| AC-3 | DB seeded to ≥ 100k rows; `VACUUM ANALYZE tiles` run | Execute the EXPLAIN probe | Plan contains `Index Only Scan using tiles_leaflet_path`; `Heap Fetches` ≤ 1 | NFR-Perf-1 | +| AC-4 | DB seeded with 2500 `(z, x, y)` cells × ~3 versions each | POST inventory with 2500 coords, repeat 20 times | p95 ≤ 1000 ms | NFR-Perf-2 | +| AC-5 | API running with `Http1AndHttp2` enabled | `HttpClient` with `Version20` + `RequestVersionExact` fires 20 concurrent GET `/tiles/...` | All 20 succeed; all `HttpResponseMessage.Version == 2.0`; ETag + Cache-Control unchanged | NFR-Compat-1 | +| AC-6 | API running | Probe both-populated / neither-populated / 5001-entry / no-JWT requests | 400 / 400 / 400 / 401 with descriptive details | — | + +## Constraints + +- **No column renames**: keep `tile_zoom`, `tile_x`, `tile_y`, `latitude`, `longitude`, `location_hash`, `flight_id`, `content_sha256` as named today. +- **No new migration column** in scope — migration 015 is index-only (covering index + drop of the superseded `idx_tiles_location_hash`). All required columns landed in AZ-503-foundation migration 014. +- **Migration 015 must be reversible** by dropping the new index (`tiles_leaflet_path`) and recreating `idx_tiles_location_hash`; document the back-migration in the SQL comment header. +- **Cross-repo invariant**: `Uuidv5.TileNamespace` (`5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c`) is consumed by this task (every inventory request without pre-computed hashes recomputes the UUIDv5 server-side). It MUST byte-match the same constant in `gps-denied-onboard/components/c6_tile_cache/_uuid.py:TILE_NAMESPACE` plus reference-vector tests on BOTH sides — the satellite-provider side already has `Uuidv5Tests` (cycle 5); the sibling-repo side is the gps-denied-onboard workspace's own PBI and is NOT in this task's deliverables. Surfacing this here per cycle-5 retro decision-item #10 (the formal `workspace:` field on cross-repo ACs is deferred to a separate skill-update PBI; this task uses inline-Constraint capture per /autodev cycle-6 Step 9 user choice). +- **Feature flag gate on the onboard consumer side**: `gps-denied-onboard` AZ-316 must keep `c11.use_bulk_list_endpoint=false` default until this PBI is deployed to the target environment. This is a sibling-repo obligation; surfaced for awareness, not in this task's deliverables. +- **Architecture Vision compliance**: the new endpoint slots cleanly into the existing layered architecture (API → Services → DataAccess → PostgreSQL); no new components introduced; the new repo method follows the existing `TileRepository` pattern. No Architecture Vision principle is violated. + +## Risks & Mitigation + +**Risk 1: Covering-index INCLUDE columns are tighter than the original AZ-503 inventory query implied** +- *Risk*: The original AZ-503 spec named `INCLUDE (file_path, content_type, etag, voting_status)`. Of those, `content_type` is named `image_type` in the actual schema, `etag` is not a column (computed from headers/body), and `voting_status` does not exist. The Jira AZ-505 spec narrows to `INCLUDE (file_path, source)`. The inventory endpoint's response wants more fields (`captured_at`, `flight_id`, `id`) which are NOT in the INCLUDE list and therefore trigger heap fetches for inventory requests. +- *Mitigation*: AC-3's "index-only" target applies only to the Leaflet hot path (`SELECT file_path FROM tiles WHERE location_hash = $1 LIMIT 1`), which the narrow INCLUDE serves perfectly. The inventory endpoint legitimately needs the heap fetch for richer fields; AC-4's 1000 ms / 2500 tiles budget accounts for this. If post-implementation profiling shows inventory heap-fetch cost is the bottleneck, widen INCLUDE in a follow-up PBI — do NOT pre-optimise here. + +**Risk 2: Migration 015 lock window on a populated `tiles` table** +- *Risk*: `CREATE INDEX` (without CONCURRENTLY) takes an `ACCESS SHARE` + `SHARE` lock for the duration of the build, blocking writes. Production deploy could stall UAV uploads + Google Maps downloads. +- *Mitigation*: Investigate whether DbUp can execute a non-transactional `CREATE INDEX CONCURRENTLY` statement (DbUp historically wraps each script in a transaction, which is incompatible with CONCURRENTLY). If yes — use it. If no — document the expected lock window in the migration header and the deploy runbook, and align deployment to a low-traffic window. + +**Risk 3: HTTP/2 over plaintext (h2c) may not be reachable from all clients** +- *Risk*: Browsers do NOT support h2c (HTTP/2 over plaintext) — they require ALPN + TLS. Only programmatic clients (httpx with `http2=True`, .NET `HttpClient` configured for `Version20`, Go `net/http2`) can use the multiplexed endpoint. Leaflet in a browser will continue to use HTTP/1.1 + up-to-6 connections. +- *Mitigation*: Document this in `tile-inventory.md` v1.0.0 contract and in the deploy runbook. The onboard consumer (httpx-based) IS the primary beneficiary of HTTP/2 here; browser Leaflet performance is unaffected (heap-eliminated read path via the covering index is the win there). + +**Risk 4: Onboard `TileDownloader` (AZ-316) calls inventory before this task lands in production** +- *Risk*: Ordering — onboard AZ-316 might be implemented in the sibling workspace before this PBI deploys. Production calls hit 404. +- *Mitigation*: Onboard side has a fallback path (per-tile GET via `/tiles/{z}/{x}/{y}`); the `c11.use_bulk_list_endpoint=false` feature flag is the documented gate. This is a sibling-workspace concern; surfacing here for cross-cycle visibility. + +## Contract + +This task produces TWO contract artifacts: +1. **New**: `_docs/02_document/contracts/api/tile-inventory.md` v1.0.0 — the new `POST /api/satellite/tiles/inventory` shape, body XOR validation, response field reference, ordering invariant, max-entries cap, HTTP/2-multiplex note. +2. **Major bump**: `_docs/02_document/contracts/data-access/tile-storage.md` v1.0.0 → v2.0.0 — per architecture.md ("v2.0.0 bump tracking the AZ-503 identity columns is deferred to AZ-505 when the new identity surface freezes for external consumers"). Captures: AZ-503-foundation columns (`flight_id`, `location_hash`, `content_sha256`, `legacy_id`); `idx_tiles_unique_identity` integer UPSERT key replacing the AZ-484 float key; `tiles_leaflet_path` covering index; new `location_hash`-keyed read selection rule on `GetByTileCoordinatesAsync`. + +Consumers of `tile-storage.md` v1.0.0 listed in its header (AZ-485 / future SatAR) MUST be reviewed at v2.0.0 bump time — see contract's Consumer tasks field. + +## References + +- `_docs/02_tasks/done/AZ-503_tile_identity_uuidv5_bulk_list.md` — parent spec with the original 12 ACs (AZ-505 inherits AC-5/AC-6/AC-9/AC-10/AC-12 renumbered as AZ-505 AC-1/AC-2/AC-4/AC-3/AC-5 respectively). +- `_docs/06_metrics/retro_2026-05-12_cycle5.md` § Action 2 — split rationale and 5 SP upper-bound estimate (now refined to 3 SP per Jira ticket + dependencies table). +- `_docs/02_document/architecture.md` — names AZ-505 as owner of `tile-storage.md` v2.0.0 freeze. +- `_docs/02_document/contracts/data-access/tile-storage.md` v1.0.0 — current contract to be bumped. +- `_docs/02_document/contracts/api/uav-tile-upload.md` v1.1.0 — already-bumped sibling contract (cycle 5; for reference shape only, AZ-505 does not modify it). +- `SatelliteProvider.DataAccess/Migrations/014_AddTileIdentityColumns.sql` — comment header explicitly states "the larger covering index `tiles_leaflet_path` is owned by AZ-505". +- `SatelliteProvider.Common/Utils/Uuidv5.cs` — cross-repo invariant location (`TileNamespace` constant + reference vectors). +- `gps-denied-onboard/_docs/02_tasks/todo/AZ-316_c11_tile_downloader.md` — onboard consumer of the inventory endpoint (sibling-repo concern). +- Jira AZ-505 — authoritative Jira ticket (https://denyspopov.atlassian.net/browse/AZ-505). diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index 1f34cbb..49de61e 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -2,8 +2,8 @@ ## Current Step flow: existing-code -step: 9 -name: New Task +step: 10 +name: Implement status: not_started sub_step: phase: 0