# 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<Guid> 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** — *resolved post-implementation:* Kestrel was switched to TLS on `https://+:8080` with a self-signed dev cert (`./certs/api.pfx`, generated idempotently by `scripts/run-tests.sh`) so ALPN can advertise `h2` to **both** browsers and programmatic clients. Browser Leaflet now multiplexes over the same TLS connection as the .NET / httpx clients. Production termination remains expected at the ingress (Envoy / nginx / ALB); the in-process TLS cert is dev-only and is gitignored.
- *Risk (original wording, preserved for traceability)*: 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).
