# Traceability Matrix

## Acceptance Criteria → Test Mapping

| AC | Description | Tests | Coverage |
|----|-------------|-------|----------|
| T1 | Tiles cached, not re-downloaded | BT-02 | ✓ |
| T2 | Concurrent download limit | RS-05, RL-03 | ✓ |
| T3 | Tile stored with correct path | BT-01 | ✓ |
| T4 | Tile metadata persisted | BT-01 | ✓ |
| R1 | Region state transitions | BT-03, BT-04, BT-05 | ✓ |
| R2 | CSV manifest generated | BT-03, BT-04, BT-05 | ✓ |
| R3 | Summary file generated | BT-03, BT-04, BT-05 | ✓ |
| R4 | Stitched image when requested | BT-05 | ✓ |
| R5 | Stitched image valid content | BT-05 | ✓ |
| R6 | Region processing bounded | RL-04 | ✓ |
| RT1 | Points interpolated at ~200m | BT-06 | ✓ |
| RT2 | Point types correctly assigned | BT-06 | ✓ |
| RT3 | Total distance calculated | BT-06 | ✓ |
| RT4 | Geofence filtering applied | BT-11 | ✓ |
| RT5 | ZIP ≤ 50 MB | BT-09, RL-01 | ✓ |
| RT6 | Route map stitched | BT-08, BT-10, BT-12 | ✓ |
| A1 | Region request returns immediately | BT-03 | ✓ |
| A2 | Status endpoint reflects state | BT-03, BT-07 | ✓ |
| A3 | Route returns computed metadata | BT-06 | ✓ |
| S1 | Migrations run on startup | RS-02 | ✓ |
| S2 | Queue rejects when full | RS-04, RL-02 | ✓ |
| S3 | Failed regions marked failed | RS-03 | ✓ |
| AZ-484 AC-1 | Schema accepts source + captured_at; multi-source rows coexist under 5-col unique index | `MultiSourceInsertCoexistsUnderNewIndex_AZ484_AC1`, `NewUniqueConstraintIncludesSourceColumn_AZ484_AC1` (integration) | ✓ |
| AZ-484 AC-2 | Read returns most-recent across sources | `MostRecentAcrossSourcesSelection_AZ484_AC2` (integration) | ✓ |
| AZ-484 AC-3 | Same-source UPSERT collapses to one row with refreshed captured_at | `SameSourceUpsertReplacesPreviousRow_AZ484_AC3` (integration) | ✓ |
| AZ-484 AC-4 | Migration 013 backfill leaves no orphans (count preserved, source='google_maps', captured_at=created_at) | `BackfillUpdateAssignsGoogleMapsAndCapturedAt_AZ484_AC4` (integration) | ✓ |
| AZ-484 AC-5 | Google Maps download path stamps Source='google_maps' (wire) + CapturedAt UTC | `BuildTileEntity_SetsGoogleMapsSourceAndUtcCapturedAt_AZ484_AC5` (unit) | ✓ |
| AZ-484 AC-6 | Existing region/route flows unchanged post-T1 (200 unit + smoke baseline preserved) | Full unit suite (213 tests) + integration smoke scenarios BT-01..BT-12 | ✓ |
| AZ-484 AC-7 | Vision + contract docs amended (architecture.md, glossary.md, module-layout.md, tile-storage.md frozen v1.0.0) | doc-state AC; verified by `monorepo-document` reviews | ✓ |
| AZ-487 AC-1 | Anonymous request returns 401 from every authenticated endpoint | SEC-05 (blackbox); `JwtIntegrationTests.AnonymousRequest_*_Returns401` (integration) | ✓ |
| AZ-487 AC-2 | Expired token returns 401; no internal leak in body | SEC-06 (blackbox); `JwtIntegrationTests.ExpiredToken_Returns401` (integration) | ✓ |
| AZ-487 AC-3 | Tampered signature returns 401 | SEC-07 (blackbox); `JwtIntegrationTests.InvalidSignature_Returns401` (integration) | ✓ |
| AZ-487 AC-4 | Valid token reaches handler with identical response | SEC-09, BT-18 (blackbox); `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` (integration) | ✓ |
| AZ-487 AC-5 | Startup fails on missing / short `JWT_SECRET` | SEC-08 (behavioral); `AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_Throws*` (unit) | ✓ |
| AZ-487 AC-6 | `HttpContext.User` exposes claims (`sub`, `permissions`, …) | `JwtTokenFactoryTests.Create_WithExtraClaims_PropagatesClaimsThroughValidation` (unit) + indirect via AZ-488 AC-6 (live permission check) | ✓ |
| AZ-487 AC-7 | Swagger UI Authorize button works | `JwtIntegrationTests.SwaggerDocument_AdvertisesBearerSecurityScheme` (integration; programmatic equivalent of UI flow) | ◐ doc-verified |
| AZ-487 AC-8 | All existing tests pass with attached test token | Full `scripts/run-tests.sh --full` run (cycle 2 Step 11 — passed) | ✓ |
| AZ-488 AC-1 | Happy-path 1-item batch persists with `source='uav'` | BT-13 (blackbox); `UavUploadTests.HappyPathSingleItem_PersistsRow` (integration) | ✓ |
| AZ-488 AC-2 | 3-item mixed batch returns per-item results | BT-14 (blackbox); `UavUploadTests.MixedBatch_ReturnsPerItemResults` (integration) | ✓ |
| AZ-488 AC-3 | UAV upload coexists with pre-seeded `google_maps` row | BT-15 (blackbox); `UavUploadTests.MultiSourceCoexistence_AZ484_Cycle2` (integration); reuses AZ-484 AC-1 + AC-2 invariants | ✓ |
| AZ-488 AC-4 | Same-source UPSERT keeps one `source='uav'` row | BT-16 (blackbox); `UavUploadTests.SameSourceUpsert_AZ484_Cycle2` (integration); reuses AZ-484 AC-3 invariant | ✓ |
| AZ-488 AC-5 | Unauthenticated upload returns 401 (covered by AZ-487) | `UavUploadTests.NoToken_Returns401` (integration); AZ-487 AC-1 row covers contract | ✓ |
| AZ-488 AC-6 | Authenticated request without `GPS` permission returns 403 | SEC-10 (blackbox); `UavUploadTests.ValidTokenWithoutGpsPermission_Returns403` (integration); `PermissionsRequirementTests` (unit) | ✓ |
| AZ-488 AC-7a | `INVALID_FORMAT` reject reason on wrong content-type or magic bytes | `UavTileQualityGateTests.Validate_NonJpegContentType_*` and `Validate_WrongMagicBytes_*` (unit) | ✓ |
| AZ-488 AC-7b | `SIZE_OUT_OF_BAND` reject reason on bytes outside `[MinBytes, MaxBytes]` | RL-06 (resource-limit); `UavTileQualityGateTests.Validate_BytesBelowMin_*` and `Validate_BytesAboveMax_*` (unit) | ✓ |
| AZ-488 AC-7c | `WRONG_DIMENSIONS` reject reason on non-256×256 images | BT-14, BT-17 (blackbox); `UavTileQualityGateTests.Validate_WrongDimensions_*` (unit) | ✓ |
| AZ-488 AC-7d | `CAPTURED_AT_FUTURE` / `CAPTURED_AT_TOO_OLD` reject reasons | `UavTileQualityGateTests.Validate_CapturedAtFuture_*` and `Validate_CapturedAtTooOld_*` (unit) | ✓ |
| AZ-488 AC-7e | `IMAGE_TOO_UNIFORM` reject reason on uniform / low-variance JPEGs | `UavTileQualityGateTests.Validate_UniformGreyImage_RejectsImageTooUniform` (unit) | ✓ |
| AZ-488 AC-7 (ordering) | First-applicable rule wins (e.g. format-fail beats dimensions-fail) | BT-17 (blackbox); `UavTileQualityGateTests.Validate_MultipleViolations_*` (unit) | ✓ |
| AZ-488 AC-8 | Oversized batch (> `MaxBatchSize`) returns 400 envelope error | RL-05 (resource-limit); `UavUploadTests.OversizedBatch_Returns400` (integration) | ✓ |
| AZ-488 AC-9 | Contract `uav-tile-upload.md` v1.0.0 frozen and matches implementation | doc-state AC; verified by Step 13 (Update Docs) review | ✓ |
| AZ-488 AC-10 | All existing tests + new AZ-487/AZ-488 tests pass; no AZ-484 regression | Full `scripts/run-tests.sh --full` run (cycle 2 Step 11 — passed) | ✓ |
| AZ-494 AC-1 | Wrong `iss` token returns 401 | SEC-12 (blackbox); `JwtIntegrationTests.WrongIssuer_Returns401` (integration) | ✓ |
| AZ-494 AC-2 | Wrong `aud` token returns 401 | SEC-13 (blackbox); `JwtIntegrationTests.WrongAudience_Returns401` (integration) | ✓ |
| AZ-494 AC-3 | Matching iss + aud accepted | `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` (integration; updated to mint via env iss/aud) | ✓ |
| AZ-494 AC-4 | Missing config fails fast | `AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_ThrowsOnMissingIssuer` + `_ThrowsOnEmptyIssuer` + `_ThrowsOnMissingAudience` + `_ThrowsOnEmptyAudience` (unit) | ✓ |
| AZ-494 AC-5 | Existing tests pass with matched fixtures | Full integration suite reruns at Step 16 with `JwtTestHelpers.MintAuthenticated` (auto-fills iss/aud from env) | ✓ (gate verified at Step 16) |
| AZ-494 AC-6 | Security artifacts updated (F-AUTH-2 → Resolved) | `_docs/05_security/security_report.md` + `owasp_review.md` updated this batch | ✓ |
| AZ-494 AC-7 | Suite contract reflects reality | `suite/_docs/10_auth.md` lives outside this workspace; this cycle's deploy report documents that satellite-provider validates iss/aud locally and the prod values are admin-team-confirmed at deploy time | ◐ deferred (cross-repo write) |
| AZ-491 AC-1 | Single source of truth — only one `JwtTokenFactory` exists in source | Structural: repo-wide grep returns exactly `SatelliteProvider.TestSupport/JwtTokenFactory.cs`; the legacy `SatelliteProvider.Tests/TestUtilities/JwtTokenFactory.cs` was deleted in batch 02 | ✓ |
| AZ-491 AC-2 | Existing integration tests pass unchanged | Full integration suite at Step 11 (`./scripts/run-tests.sh --full`) — all green | ✓ |
| AZ-491 AC-3 | Existing unit tests pass unchanged | Unit suite at Step 11 (Step 1 of `run-tests.sh`) — all green | ✓ |
| AZ-491 AC-4 | Runner-side concerns preserved in `JwtTestHelpers` (env reads, HttpClient mutation stay in IntegrationTests) | Structural: `JwtTokenFactory` (pure) in TestSupport; `JwtTestHelpers` (side-effectful) in IntegrationTests — documented in `module-layout.md` | ✓ |
| AZ-491 AC-5 | Cycle-2 fixes remain effective (AZ-487/AZ-488 token-validation invariants preserved) | Integration scenarios `JwtIntegrationTests.AnonymousRequest_*`, `_ExpiredToken_Returns401`, `_InvalidSignature_Returns401`, `_ValidToken_Returns200_OnHealthyEndpoint`, `UavUploadTests.*` — all migrated to `MintAuthenticated` and still PASS at Step 11 | ✓ |
| AZ-491 AC-6 | Code-review rule lands to prevent re-duplication | `.cursor/skills/code-review/SKILL.md` Phase 6 rule added in batch 02 (Cycle-3 review SKILL update) | ✓ |
| AZ-493 AC-1 | Empty-state on startup — no leftover rows from previous run | `IntegrationTestDatabaseReset.ResetAsync` invoked at runner start; uniqueness assumptions in `UavUploadTests` (`source='uav'` rows per coordinate) hold without the wall-clock workaround | ✓ |
| AZ-493 AC-2 | Wallclock workaround no longer needed | Structural: `UavUploadTests` no longer offsets coordinates by `DateTime.UtcNow.Ticks % …` to dodge stale rows; coordinates are now deterministic per scenario | ✓ |
| AZ-493 AC-3 | Opt-out preserves state (`--keep-state` flag skips reset) | `scripts/run-tests.sh` parses `--keep-state`, sets `INTEGRATION_TEST_DB_RESET=skip`, and `Program.cs` honours that env var | ✓ |
| AZ-493 AC-4 | Reset only fires in test environment (two-guard model) | Unit: `IntegrationTestResetGuardTests` (env sentinel + Host allowlist `postgres`/`localhost`/`127.0.0.1`; production-shape hostnames rejected) | ✓ |
| AZ-493 AC-5 | Documentation reflects new convention | doc-state AC — `_docs/02_document/module-layout.md` + `_docs/02_document/modules/tests_integration.md` updated in batch 03 | ✓ |
| AZ-493 AC-6 | Existing tests pass unchanged | Full integration suite at Step 11 — all green | ✓ |
| AZ-495 AC-1..AC-N | Doc folder convention formalized | doc-state AC — `.cursor/skills/new-task/SKILL.md` updated in batch 01; `_docs/02_document/module-layout.md` carries the convention | ✓ |
| AZ-496 AC-1 | `Microsoft.AspNetCore.Authentication.JwtBearer` bumped 8.0.21 → 8.0.25 in `SatelliteProvider.Api.csproj` | Structural: csproj diff visible in batch 01 commit; transitive update propagates to `Tests.csproj` via `ProjectReference` | ✓ |
| AZ-496 AC-2..AC-N | Suite still green at the new version | Full unit + integration suite at Step 11 — all green; SEC-05..SEC-11 + AZ-494 AC-1/AC-2 (which depend on `JwtBearer`) all PASS | ✓ |
| AZ-500 AC-1 | Every csproj targets `net10.0` | Structural: `grep -r "<TargetFramework>" --include="*.csproj"` returns 9/9 `net10.0`, 0 `net8.0` (verified at cycle 4 Step 11) | ✓ |
| AZ-500 AC-2 | `global.json` `sdk.version=10.0.0`, `rollForward=latestMinor` | Structural: file contents asserted; SDK roll-forward exercised by host running .NET 10.0.103 | ✓ |
| AZ-500 AC-3 | All Docker base images + CI images on `:10.0` | Structural: `grep -rE "mcr.microsoft.com/dotnet/" --include="*Dockerfile" --include="*.yml" --include="*.sh"` → 7/7 on `:10.0` | ✓ |
| AZ-500 AC-4 | `Microsoft.AspNetCore.*` + `Microsoft.Extensions.*` on `10.0.7`; `Serilog.AspNetCore` documented fallback `8.0.3` | Structural: csproj diff (19 references on `10.0.7`); Serilog.AspNetCore fallback rationale recorded in `AGENTS.md:244` per Risk #4 | ✓ |
| AZ-500 AC-5 | Perf-script bootstrap step succeeds (no exit 3) — closes cycle-3 SDK-mismatch leftover | `PERF_REPEAT_COUNT=2 PERF_UAV_BATCH_SIZE=2 ./scripts/run-performance-tests.sh` exit 1 (NOT 3 — bootstrap clean, build OK, JWT mint OK, PT-01..PT-07 PASS); leftover `_docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` updated with new (non-SDK) PT-08 grep-pipefail finding; full perf gate runs at Step 15 of cycle 4 | ✓ |
| AZ-500 AC-6 | All unit + integration tests pass on the migrated build | Full `./scripts/run-tests.sh --full` at cycle 4 Step 11 — 271/271 unit + integration suite green | ✓ |
| AZ-500 AC-7 | `docker-compose build` succeeds with no downgrade / framework / missing-image warnings | `run-tests.sh` Step 2 build path + `docker compose up -d --build` both succeeded; only warnings emitted are CS8604 nullable + ASPDEPR002 deprecation (neither category gated) | ✓ |
| AZ-500 AC-8 | Documentation reflects .NET 10 | `_docs/02_document/architecture.md` lines 5 + 67 (Tech Stack table) updated; `AGENTS.md` lines 9 + 240–244 updated incl. Serilog fallback note | ✓ |
| AZ-503 AC-1 | UUIDv5 reference vectors match Python (≥10 cases) | `Uuidv5Tests.Create_MatchesPythonReferenceVectors_AC1` (unit) + version/variant bit assertions | ✓ |
| AZ-503 AC-2 | Insert is idempotent on identical inputs (id stable, created_at preserved) | BT-20 (blackbox); `UavTileUploadHandlerTests.HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (unit) | ✓ |
| AZ-503 AC-3 | Multi-flight UAV uploads coexist (two ids, shared location_hash) | BT-19 (blackbox); `UavTileUploadHandlerTests.HandleAsync_TwoFlightsSameCell_ProduceDistinctIdsAndPathsButSameLocationHash` (unit); `UavUploadTests.MultiFlightUavRowsCoexist_AZ503_AC3` (integration) | ✓ |
| AZ-503 AC-4 | Float-rounded coordinates collapse to a single row | BT-21 (blackbox); `UavUploadTests.FloatRoundingDoesNotBreakIdempotence_AZ503_AC4` (integration) | ✓ |
| 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-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 | 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` | 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 | 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'`) | 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}` | 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-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-4 | Leftover `_docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` deleted on green full run | Verified at autodev Step 15 by `test -f _docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` returning non-zero after the green run + commit | ◐ gate at Step 15 |
| AZ-794 AC-1 | Inventory request body uses short names `{z, x, y}` (OSM convention) | BT-27 sub-case `pos` (`TileInventoryValidationTests.HappyPath_Returns200`); existing AZ-505 integration suite (`TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` + the AC-6 validation tests already use z/x/y after the rename); deserializer-level proof via BT-27 sub-case `9c` (`OldV1FieldName_Returns400`) — old names now hard-fail | ✓ |
| AZ-794 AC-2 | Inventory response body uses short names `{z, x, y}`; all other fields (`locationHash`, `present`, `id`, `capturedAt`, `source`, `flightId`, `resolutionMPerPx`) unchanged byte-for-byte from the pre-rename contract | `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` (integration; asserts `entry.Z`, `entry.X`, `entry.Y`, `entry.LocationHash`, `entry.Present`, `entry.Id`, `entry.CapturedAt`, `entry.Source` for 25 mixed present/absent entries against the v2.0.0 wire shape) | ✓ |
| AZ-794 AC-3 | OpenAPI / Swagger spec declares `z, x, y` (not the old names) as the required coordinate properties | Doc-state AC — verified at Step 13 (Update Docs) review against `/swagger/v1/swagger.json` schema definitions for `TileInventoryRequest` + `TileCoord` | ◐ doc-verified at Step 13 |
| AZ-794 AC-4 | `_docs/02_document/contracts/api/tile-inventory.md` bumped to v2.0.0; Migration / Coexistence section names AZ-794 and the breaking-rename | Doc-state AC — `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 Change Log entry naming AZ-794 (verified at Step 13 Update Docs review) | ✓ |
| AZ-795 (epic) | Shared input-validation infra in place: FluentValidation 12.0.0 + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + camelCase naming policy + new `error-shape.md` v1.0.0 contract | Structural: `SatelliteProvider.Api/Validators/{ValidationEndpointFilter,GlobalValidatorConfig,ValidationEndpointFilterExtensions}.cs` + `SatelliteProvider.Api/GlobalExceptionHandler.cs` exist; `error-shape.md` v1.0.0 frozen with 8 documented test cases; end-to-end exercise via the AZ-796 test suite (every BT-27 sub-case routes through this infra). Per-endpoint child task tracker (`AZ-796` is the first; siblings to follow) is owned by Jira AZ-795. | ✓ (infra + contract; per-endpoint child coverage tracked individually) |
| AZ-796 AC-1 | Each of the 9 validation rules rejects with HTTP 400 + RFC 7807 ProblemDetails; `errors[]` array has single-rule precision (no unrelated rules) | BT-27 (blackbox; sub-cases 1, 2a, 2b, 3, 4, 5a, 5b, 6a, 6b, 7, 8a, 8b, 9a, 9b, 9c — one per rule); `TileInventoryValidationTests.*` (integration: 15 failure tests) + `InventoryRequestValidatorTests.*` (unit: covers the rules expressible at the validator layer in isolation) | ✓ |
| AZ-796 AC-2 | Happy path unchanged (HTTP 200 with existing result shape; one entry per requested tile, same ordering, fields preserved) | BT-27 sub-case `pos` (`TileInventoryValidationTests.HappyPath_Returns200`); no regression in existing `TileInventoryTests.OrderingAndPresentAbsentShaping_AC1` (which still passes at cycle 7 Step 11) | ✓ |
| AZ-796 AC-3 | `InventoryRequestValidator` lives in its own file under `SatelliteProvider.Api/Validators/`; xUnit class has one test method per `RuleFor(...)` (≥ 9 unit-test methods) | Structural: `SatelliteProvider.Api/Validators/InventoryRequestValidator.cs` exists (74 lines, isolated validator + `TileCoordValidator`); `SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests.cs` contains 16 test methods (`Validate_TilesPopulated_LocationHashesNull_Passes`, `Validate_LocationHashesPopulated_TilesNull_Passes`, `Validate_BothPopulated_FailsXorRule`, `Validate_NeitherPopulated_FailsXorRule`, `Validate_BothEmpty_FailsXorRule`, `Validate_TilesAtCap_Passes`, `Validate_TilesOverCap_FailsCapRule`, `Validate_LocationHashesOverCap_FailsCapRule`, `Validate_TileZoomOutOfRange_FailsRangeRule` ×3, `Validate_TileZoomInRange_PassesRangeRule` ×3, `Validate_TileXNegative_FailsRangeRule`, `Validate_TileXAtUpperBound_FailsRangeRule`, `Validate_TileYNegative_FailsRangeRule`, `Validate_TileYAtUpperBound_FailsRangeRule`, `Validate_AxesAtMaxForZoom_Passes`) | ✓ |
| AZ-796 AC-4 | Integration tests cover happy + failure per rule (≥ 10 methods) in `SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs` | `TileInventoryValidationTests` contains 16 integration test methods (1 happy + 15 failure: `HappyPath_Returns200`, `EmptyBody_Returns400`, `NeitherPopulated_Returns400`, `BothPopulated_Returns400`, `EmptyTilesArray_Returns400`, `TilesOverCap_Returns400`, `MissingZ_Returns400WithFieldPath`, `MissingXAndY_Returns400`, `ZoomOutOfRange_Returns400WithFieldPath`, `XBeyondZoomBounds_Returns400`, `YBeyondZoomBounds_Returns400`, `NegativeAxis_Returns400`, `UnknownRootField_Returns400`, `UnknownNestedField_Returns400`, `OldV1FieldName_Returns400`, `TypeMismatch_Returns400`) — 16 ≥ 10. Cycle 7 Step 11 full run reports all 16 passing. | ✓ |
| AZ-796 AC-5 | `/swagger/v1/swagger.json` marks required fields, declares integer ranges per validation rules, declares 400 response with ProblemDetails schema | Doc-state AC — verified at Step 13 (Update Docs) review against the published OpenAPI document; integration smoke is the existing `JwtIntegrationTests.SwaggerDocument_AdvertisesBearerSecurityScheme` pattern (a future analogous test against the validation schema is out-of-scope this cycle) | ◐ doc-verified at Step 13 |
| AZ-796 AC-6 | `_docs/02_document/contracts/api/tile-inventory.md` updated to document the 9 validation rules + error contract reference | Doc-state AC — `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 Change Log entry naming AZ-796 (verified at Step 13 Update Docs review) | ✓ |
| AZ-796 AC-7 | `scripts/probe_inventory_validation.sh` committed; exercises each failure mode via `curl` + JWT for documentation / regression | Structural: `scripts/probe_inventory_validation.sh` exists in repo and is manually runnable | ✓ |
| AZ-808 AC-1 | Each of the 8 region-request validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-28 sub-cases 1, 2a, 2b, 3, 4, 5, 6, 7, 8, 9, 10 (blackbox); `RegionRequestValidationTests` (integration, 11+ failure methods) + `RegionRequestValidatorTests` (unit, ≥ 8 methods covering each `RuleFor`); shared `ProblemDetailsAssertions` enforces error-shape v1.0.0 conformance | ✓ |
| AZ-808 AC-2 | Happy path unchanged — valid body returns HTTP 200 + `RegionStatusResponse`; background processing still runs; probe's 9-tile Derkachi case completes < 10 s | BT-28 sub-case `pos` (`RegionRequestValidationTests.HappyPath_Returns200`); no regression in existing `RegionRequestTests.cs` (cycle 8 Step 11 — green) | ✓ |
| AZ-808 AC-3 | `RegionRequestValidator` in its own file under `SatelliteProvider.Api/Validators/`; unit-tested (≥ 1 per `RuleFor`) | Structural: `SatelliteProvider.Api/Validators/RegionRequestValidator.cs` exists; `SatelliteProvider.Tests/Validators/RegionRequestValidatorTests.cs` covers each `RuleFor` chain | ✓ |
| AZ-808 AC-4 | `RegionRequestValidationTests.cs` covers happy + 8+ failure modes; MUST include `Post_WithMissingId_ReturnsBadRequest` reproducing 2026-05-22 silent-coercion case | `SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs` includes `MissingId_Returns400` (the renamed AZ-777 Phase 2 reproducer) and ≥ 11 total failure methods; uses `ProblemDetailsAssertions` from AZ-795 | ✓ |
| AZ-808 AC-5 | `_docs/02_document/contracts/api/region-request.md` v1.0.0 created and published | Doc-state AC — `region-request.md` v1.0.0 created in cycle-8 batch (coordinated with AZ-812 — published directly with `lat`/`lon` names per AZ-812 AC-6); verified at Step 13 (Update Docs) review | ✓ |
| AZ-808 AC-6 | `_docs/02_document/system-flows.md` F2 updated to reference the new contract doc + error shape | Doc-state AC — verified at Step 13 (Update Docs) review | ◐ doc-verified at Step 13 |
| AZ-808 AC-7 | OpenAPI marks `RequestRegionRequest` fields `required`, declares ranges, documents 400 response | Doc-state AC — verified at Step 13 (Update Docs) review against published `/swagger/v1/swagger.json`; Swashbuckle annotations match AZ-796 pattern | ◐ doc-verified at Step 13 |
| AZ-808 AC-8 | Manual probe script exercises each failure mode via `curl` + JWT | Structural: `scripts/probe_region_validation.sh` exists and is manually runnable | ✓ |
| AZ-809 AC-1 | Each of the 14 route-creation validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-29 sub-cases 1..14 (blackbox); `CreateRouteValidationTests` (integration, 14+ failure methods covering deserializer + per-DTO + per-element + cross-field layers) + `CreateRouteRequestValidatorTests` + `RoutePointValidatorTests` + `GeofencePolygonValidatorTests` (unit, ≥ 13 methods total) | ✓ |
| AZ-809 AC-1b (security-audit F-AZ809-1 follow-up; `route-creation.md` v1.0.1 Inv-10) | `geofences.polygons.Count <= 50` rejects with HTTP 400 + `errors["geofences.polygons"]` | BT-29 sub-case 9b (blackbox); `CreateRouteValidationTests.GeofencePolygonsTooMany_Returns400` (integration); `CreateRouteRequestValidatorTests.Validate_GeofencePolygonsTooMany_FailsCountRule` (unit) | ✓ |
| AZ-809 AC-2 | Happy path unchanged — valid body returns HTTP 200 + `RouteResponse`; F5 background still runs when `requestMaps=true`; probe's 2-point 132 m route completes < 20 s | BT-29 sub-case `pos` (`CreateRouteValidationTests.HappyPath_Returns200`); no regression in existing `RouteCreationTests.cs` (cycle 8 Step 11 — green) | ✓ |
| AZ-809 AC-3 | `CreateRouteRequestValidator`, `RoutePointValidator`, `GeofencePolygonValidator` each in their own files under `SatelliteProvider.Api/Validators/`; ≥ 13 unit-test methods total | Structural: three validator files exist as separate `.cs` files; unit-test files together contain ≥ 13 methods covering every `RuleFor` / `RuleForEach` chain | ✓ |
| AZ-809 AC-4 | `CreateRouteValidationTests.cs` covers happy + 13+ failure modes; MUST include `Post_WithMissingId_ReturnsBadRequest` | `SatelliteProvider.IntegrationTests/CreateRouteValidationTests.cs` includes `MissingId_Returns400` (the AZ-777 Phase 2 reproducer for route variant) and ≥ 14 total failure methods | ✓ |
| AZ-809 AC-5 | `_docs/02_document/contracts/api/route-creation.md` v1.0.0 created and published | Doc-state AC — `route-creation.md` v1.0.0 created in cycle-8 batch; verified at Step 13 review | ✓ |
| AZ-809 AC-6 | `_docs/02_document/system-flows.md` F4 + F5 updated to reference new contract doc + error shape | Doc-state AC — verified at Step 13 (Update Docs) review | ◐ doc-verified at Step 13 |
| AZ-809 AC-7 | OpenAPI marks all required fields at every nesting level, declares ranges, documents 400 response | Doc-state AC — verified at Step 13 against published `/swagger/v1/swagger.json` | ◐ doc-verified at Step 13 |
| AZ-809 AC-8 | Manual probe script exercises each failure mode via `curl` + JWT | Structural: `scripts/probe_route_validation.sh` exists and is manually runnable | ✓ |
| AZ-809 AC-9 | (Advisory) `RequestRegionRequest.sizeMeters` vs `CreateRouteRequest.regionSizeMeters` naming inconsistency surfaced for parent-suite decision | Not tested — surfaced in `_docs/03_implementation/batch_*_cycle8_report.md` for parent-suite team follow-up | ◐ advisory (not tested) |
| AZ-809 AC-10 | (Advisory) Input `points: [{lat, lon}]` vs output `points: [{latitude, longitude}]` round-trip asymmetry surfaced for parent-suite decision | Not tested — surfaced in `_docs/03_implementation/batch_*_cycle8_report.md` for parent-suite team follow-up | ◐ advisory (not tested) |
| AZ-810 AC-1 | Each of the 14 upload-metadata validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-30 sub-cases 1..13 (blackbox); `UavUploadValidationTests` (integration, ≥ 13 failure methods covering deserializer + FluentValidation + envelope cross-field layers) + `UavTileMetadataValidatorTests` + `UavTileBatchMetadataPayloadValidatorTests` (unit, ≥ 11 methods total) | ✓ |
| AZ-810 AC-2 | Happy path unchanged — valid envelope returns HTTP 200 + per-item result list; per-item file rejections (`IUavTileQualityGate`) still return HTTP 200 with per-item status | BT-30 sub-case `pos` (`UavUploadValidationTests.HappyPath_Returns200`); existing AZ-488 BT-13..BT-17 + `UavUploadTests` continue green (cycle 8 Step 11 after the AZ-810 test-data coord-clamp fix in commit `b763da3`) | ✓ |
| AZ-810 AC-3 | `UavTileMetadataValidator` + `UavTileBatchMetadataPayloadValidator` each in their own files under `SatelliteProvider.Api/Validators/`; ≥ 11 unit-test methods total | Structural: two validator files plus `UavUploadValidationFilter.cs` (the multipart envelope filter) exist under `SatelliteProvider.Api/Validators/`; unit-test files contain ≥ 11 methods covering each `RuleFor` | ✓ |
| AZ-810 AC-4 | `UavUploadValidationTests.cs` covers happy + 12+ failure modes with full ValidationProblemDetails assertion | `SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs` contains ≥ 13 integration test methods spanning all three enforcement layers; uses `ProblemDetailsAssertions` | ✓ |
| AZ-810 AC-5 | `_docs/02_document/contracts/api/uav-tile-upload.md` bumped to v1.2.0 with the new validation section | Doc-state AC — `uav-tile-upload.md` v1.2.0 Change Log entry naming AZ-810; verified at Step 13 review | ✓ |
| AZ-810 AC-6 | `_docs/02_document/modules/api_program.md` documents the new multipart validation endpoint filter | Doc-state AC — `api_program.md` updated in cycle-8 batch; verified at Step 13 review | ✓ |
| AZ-810 AC-7 | OpenAPI marks `UavTileBatchMetadataPayload` + `UavTileMetadata` fields `required`, declares ranges, documents 400 response | Doc-state AC — verified at Step 13 against published `/swagger/v1/swagger.json`; `[JsonRequired]` annotations propagate to Swashbuckle as `required: [latitude, longitude, tileZoom, tileSizeMeters, capturedAt]` and `required: [items]` | ◐ doc-verified at Step 13 |
| AZ-810 AC-8 | Manual probe script exercises each failure mode via multipart `curl` + JWT | Structural: `scripts/probe_upload_validation.sh` exists, reuses the AZ-808/AZ-809/AZ-811 probe-script pattern, and is manually runnable | ✓ |
| AZ-810 AC-9 | No regression in existing AZ-488 integration tests (`UavTileBatchUploadTests.cs`, `UavTileQualityGateTests.cs`) | Cycle 8 Step 11 full integration run green AFTER fixing a pre-existing latent bug in `UavUploadTests.NextTestCoordinate` that AZ-810 exposed (seed `(Ticks/TicksPerSecond) % 1_000_000` produced lat > 90°; clamped to lat ∈ [50, 70), lon ∈ [10, 40) in commit `b763da3`). The original AC-9 verification (cycle 8 batch_04 report — "verified by tracing source") was a false-PASS; the green re-run is the binding evidence. Lesson recorded in `_docs/LESSONS.md` (2026-05-23) | ✓ (verified by full-suite re-run) |
| AZ-811 AC-1 | Each of the 5 GET-lat/lon validations rejects with HTTP 400 + ValidationProblemDetails | BT-31 sub-cases 1..5 + 4a/4b/4c (blackbox); `GetTileByLatLonValidationTests` (integration, ≥ 7 failure methods) + `GetTileByLatLonQueryValidatorTests` (unit, ≥ 3 methods) | ✓ |
| AZ-811 AC-2 | Happy path unchanged — `?lat=&lon=&zoom=` returns HTTP 200 + `DownloadTileResponse`; tile still downloaded/persisted | BT-31 sub-case `pos` (`GetTileByLatLonValidationTests.HappyPath_Returns200`); no regression in existing `TileByLatLonTests.cs` (cycle 8 Step 11 — green) | ✓ |
| AZ-811 AC-3 | `GetTileByLatLonQueryValidator` in its own file under `SatelliteProvider.Api/Validators/`; unit-tested (≥ 3 methods) | Structural: `GetTileByLatLonQueryValidator.cs` exists; unit-test file covers the 5 rules in ≥ 3 methods | ✓ |
| AZ-811 AC-4 | `GetTileByLatLonValidationTests.cs` covers happy + 4+ failure modes | `SatelliteProvider.IntegrationTests/GetTileByLatLonValidationTests.cs` contains ≥ 7 failure methods + 1 happy path; uses `ProblemDetailsAssertions` | ✓ |
| AZ-811 AC-5 | `_docs/02_document/contracts/api/tile-latlon.md` v1.0.0 created and published | Doc-state AC — `tile-latlon.md` v1.0.0 created in cycle-8 batch; verified at Step 13 review | ✓ |
| AZ-811 AC-6 | `_docs/02_document/modules/api_program.md::GetTileByLatLon Handler` updated to reference the validator + new contract doc | Doc-state AC — `api_program.md` updated in cycle-8 batch; verified at Step 13 review | ✓ |
| AZ-811 AC-7 | OpenAPI marks query params required + ranges + 400 response | Doc-state AC — verified at Step 13 against published `/swagger/v1/swagger.json` | ◐ doc-verified at Step 13 |
| AZ-811 AC-8 | Manual probe script exercises each failure mode via `curl` + JWT | Structural: `scripts/probe_latlon_validation.sh` exists and is manually runnable | ✓ |
| AZ-811 AC-9 | The novel `UnknownQueryParameterEndpointFilter` (rule 4 — unknown-query-param rejection) is documented in `_docs/02_document/modules/api_program.md` so the next query-param endpoint can reuse it | Doc-state AC — the filter's behavior + reuse contract documented in `api_program.md`; verified at Step 13 review. BT-31 sub-cases `4b` (legacy `?Latitude=&Longitude=&ZoomLevel=` rejected) and `4c` (hostile `?debug=1&admin=true` rejected) prove the filter works as documented | ✓ |
| AZ-812 AC-1 | `RequestRegionRequest` DTO uses `Lat` / `Lon` (C#) + `[JsonPropertyName("lat")]` / `[JsonPropertyName("lon")]` | Structural: `SatelliteProvider.Common/DTO/RequestRegionRequest.cs` diff shows the rename + JsonPropertyName attributes; sibling DTOs `RoutePoint` and `GeoPoint` already used `lat`/`lon` (no change there) | ✓ |
| AZ-812 AC-2 | Wire format is `{"lat":..,"lon":..}` end-to-end (request body, OpenAPI schema, docs, all integration tests) | BT-28 sub-case `pos` exercises the post-rename wire shape; integration test `RegionRequestValidationTests` + `RegionRequestTests` use `lat`/`lon` in every body; `region-request.md` v1.0.0 ships with `lat`/`lon` from day one (AZ-812 AC-6 coordination with AZ-808); OpenAPI verified at Step 13 | ✓ |
| AZ-812 AC-3 | `RegionTests.cs` happy-path tests pass against new wire format | Cycle 8 Step 11 full run — green; all `RegionRequestTests` updated to send `lat`/`lon` in the same commit as the DTO rename | ✓ |
| AZ-812 AC-4 | `curl` probe with `{"id":"<guid>","lat":49.94,"lon":36.31,"sizeMeters":200,"zoomLevel":18,"stitchTiles":false}` returns HTTP 200 + valid `regionId`; old `{"latitude":..,"longitude":..}` returns HTTP 400 with `UnmappedMemberHandling.Disallow` rejecting the unknown fields | BT-28 sub-case `pos` (new names accepted) + sub-case `9` (`OldLatLongNames_Returns400` — old `latitude`/`longitude` rejected as unknown). The strict-deserializer behavior is what AZ-795's `UnmappedMemberHandling.Disallow` makes possible; pre-cycle-8 the rename would have silently coerced old names to `Lat=0, Lon=0` | ✓ |
| AZ-812 AC-5 | Docs updated: `common_dtos.md`, `api_program.md`, `system-flows.md` (F2) | Doc-state AC — all three files updated in cycle-8 batch; verified at Step 13 review | ◐ doc-verified at Step 13 |
| AZ-812 AC-6 | Contract doc coordination: `region-request.md` v1.0.0 published directly with `lat`/`lon` (because AZ-808 + AZ-812 shipped in same cycle) — no `v1.0.0 → v2.0.0` bump needed | Doc-state AC — `region-request.md` v1.0.0 Change Log section names both AZ-808 (validation rules) and AZ-812 (`lat`/`lon` field names); verified at Step 13 review | ✓ |

## Restrictions → Test Mapping

| Restriction | Tests | Coverage |
|-------------|-------|----------|
| .NET 10 runtime (cycle 4 — was .NET 8.0 LTS through cycle 3) | All (via Docker image `mcr.microsoft.com/dotnet/aspnet:10.0`); cycle 4 Step 11 full suite green | ✓ |
| PostgreSQL 16 | All (via docker-compose) | ✓ |
| Single instance | PT-05 (concurrent regions on one instance) | ✓ |
| Max 4 concurrent downloads | RS-05, RL-03 | ✓ |
| Max 20 concurrent regions | RL-04 | ✓ |
| Queue capacity 1000 | RS-04, RL-02 | ✓ |
| Max ZIP 50 MB | RL-01 | ✓ |
| ~~No authentication~~ → JWT (HS256) on every endpoint, `GPS` permission on UAV upload | SEC-01..SEC-04 (input handling); SEC-05..SEC-09 (AZ-487 JWT layer); SEC-10..SEC-11 (AZ-488 permission + leak hygiene) | ✓ (superseded by AZ-487 + AZ-488, cycle 2) |

## NFRs → Test Mapping

| NFR | Source | Tests | Coverage |
|-----|--------|-------|----------|
| AZ-484 Perf — `GetTilesByRegionAsync` p95 ≤ 1.10 × pre-AZ-484 baseline | AZ-484 task spec § Non-Functional Requirements | PT-07 (Implemented in AZ-492 — cold + warm distribution, p50/p95 reported; cross-commit baseline comparison remains operator-driven at Step 15) | ✓ |
| AZ-484 Compatibility — no public HTTP response field added/removed; vestigial `maps_version`/`version` columns preserved (nullable) | AZ-484 task spec § Non-Functional Requirements | Existing integration suite (no API contract change observable); BT-01 / region status responses verify response shape | ✓ |
| AZ-487 Performance — JWT validation < 1 ms overhead per request | AZ-487 task spec § Non-Functional Requirements | Not separately measured (HMAC-SHA256 + claims parse is sub-millisecond on any modern x86; no caching needed). Re-measure if PT-07 / PT-08 (AZ-492 harness) shows aggregate regression. | ◐ recorded |
| AZ-487 Security — `RequireSignedTokens`, `RequireExpirationTime`, `ClockSkew = 30 s`, secret ≥ 32 bytes, `iss` + `aud` validated (extended by AZ-494) | AZ-487 + AZ-494 task specs § Non-Functional Requirements + Constraints | `AuthenticationServiceCollectionExtensionsTests` (unit) + SEC-05..SEC-09 + AZ-494 AC-1/AC-2 wrong-iss/aud (integration) | ✓ |
| AZ-487 Reliability — Fail-fast on missing / short `JWT_SECRET` at startup (extended by AZ-494 to iss + aud) | AZ-487 + AZ-494 task specs § Non-Functional Requirements | SEC-08 (behavioral) + unit `AddSatelliteJwt_ThrowsOnMissingSecret` + `_ThrowsOnMissingIssuer` + `_ThrowsOnMissingAudience` | ✓ |
| AZ-488 Performance — Per-item gate cost < 50 ms; p95 batch-of-10 < 2 s | AZ-488 task spec § Non-Functional Requirements | PT-08 (Implemented in AZ-492 — 20-batch distribution, batch p95 gated at 2000 ms; per-item gate cost reported as derived proxy `batch_p95 / batch_size`. True per-call `UavTileQualityGate.Validate` timing requires server-side instrumentation — follow-up). | ✓ (batch p95) / ◐ (per-item proxy only) |
| AZ-488 Reliability — File-first then DB row; per-item failures never fail the batch envelope (except 400/401/403) | AZ-488 task spec § Non-Functional Requirements | BT-14 (mixed-batch shows per-item isolation); `UavTileUploadHandlerTests.*PersistAsync*` (unit); reject reason `STORAGE_FAILURE` defined in contract for the orphan-row recovery path | ✓ |
| AZ-488 Compatibility — Replaces 501 stub; coexists with AZ-484 `tile-storage` v1.0.0 contract on the write side | AZ-488 task spec § Non-Functional Requirements + Contract | `StubAndErrorContractTests` updated to drop the stub-501 expectation; BT-15 + BT-16 validate the AZ-484 invariants under live UAV writes | ✓ |
| AZ-488 Security — Reject details never leak server internals; integer-only file-path construction | AZ-488 task spec § Non-Functional Requirements + Risk 2 | SEC-11 (blackbox); `UavTileFilePathTests` (unit) | ✓ |
| AZ-503 Cross-repo contract — UUIDv5 namespace pinned to `5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c`; C# and Python (`gps-denied-onboard`) MUST produce byte-identical output | AZ-503 task spec § Constraints | `Uuidv5Tests.Create_MatchesPythonReferenceVectors_AC1` covers the C# side; cross-repo (Python) side is enforced by `gps-denied-onboard` `AZ-304` and is out of this workspace's automated suite. The constant value is asserted structurally by `Uuidv5Tests` referencing `Uuidv5.TileNamespace`. | ✓ (C# side) / ◐ (Python parity verified by reference vectors; sibling workspace owns the runtime check) |
| AZ-503 Compatibility — No column renames; `tile_zoom`/`tile_x`/`tile_y`/`latitude`/`longitude` preserved; legacy `id` retained in `legacy_id` for one cycle | AZ-503 task spec § Constraints + Risk 1 | `MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull` (asserts existing column names unchanged); migration 014 SQL inspection (no DROP / RENAME COLUMN on existing columns); structural: `legacy_id` populated from `id` for all pre-existing rows | ✓ |
| AZ-503 Migration constraint — Additive non-blocking `ALTER TABLE ADD COLUMN`; backfill in-migration; `NOT NULL` set only on `location_hash` (legacy `content_sha256` left NULLable — see batch_02_cycle5_report.md "Low" finding) | AZ-503 task spec § Constraints + Risk 3 | Migration script inspection (`014_AddTileIdentityColumns.sql`); `MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull` asserts `location_hash NOT NULL`; application-layer NOT NULL invariant for new `content_sha256` writes enforced in `TileService.BuildTileEntity` + `UavTileUploadHandler.PersistAsync` | ✓ |
| AZ-503 Selection-rule preservation — AZ-484 read-side tie-break (`captured_at DESC, updated_at DESC, id DESC`) unchanged | AZ-503 task spec § Constraints | `TileRepository.GetByTileCoordinatesAsync` unchanged on the read path; AZ-484 AC-2 row above (`MostRecentAcrossSourcesSelection_AZ484_AC2`) still passes at Step 11 | ✓ |
| AZ-504 Compatibility — Fix preserves `set -e -o pipefail` globally; only the empty-grep-match case is tolerated locally; no silent error swallowing | AZ-504 task spec § Non-Functional Requirements + Constraints | Standalone shell harness (batch_01_cycle5_report.md) — case "non-zero exit code other than 1 from grep" still propagates failure; structural: `scripts/run-performance-tests.sh:13` retains `set -euo pipefail`; only the two grep counters at lines 416-417 are wrapped in `{ grep -o … \|\| true; }` (not blanket `\|\| true` over the whole assignment) | ✓ |

## Coverage Summary

| Category | Total Tests | ACs Covered | Restrictions Covered |
|----------|-------------|-------------|---------------------|
| Blackbox (positive) | 12 | 19/22 | — |
| Blackbox (negative) | 5 | — | — |
| Performance | 8 | 4 | 1 |
| Resilience | 6 | 4 | 3 |
| Security | 14 | 9 (AZ-487 AC-1..AC-7, AZ-488 AC-6, leak-hygiene NFR) + 3 (AZ-1113 AC-1..AC-3) | 1 (AZ-487 supersedes "No authentication") |
| Resource Limits | 7 | 5 | 4 |
| 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-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 (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 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). | — |
| Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename (integration + unit + blackbox + contract) | 1 integration file (`TileInventoryValidationTests`, 16 tests) + 1 unit file (`InventoryRequestValidatorTests`, 16 tests) + 1 blackbox (BT-27 with 16 sub-cases) + 1 new contract (`error-shape.md` v1.0.0) + 1 bumped contract (`tile-inventory.md` v2.0.0) | 12/12 in-scope (AZ-794 AC-1..AC-4, AZ-795 epic-level, AZ-796 AC-1..AC-7); 2 ACs (AZ-794 AC-3 + AZ-796 AC-5) are `◐ doc-verified at Step 13`. | — |
| Cycle 8 — AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 strict validation sweep + region OSM rename (integration + unit + blackbox + contracts) | 4 integration files (`RegionRequestValidationTests`, `CreateRouteValidationTests`, `UavUploadValidationTests`, `GetTileByLatLonValidationTests` — ≥ 45 failure methods + 4 happy paths) + 5 unit files (`RegionRequestValidatorTests`, `CreateRouteRequestValidatorTests`, `RoutePointValidatorTests`, `GeofencePolygonValidatorTests`, `UavTileMetadataValidatorTests`, `UavTileBatchMetadataPayloadValidatorTests`, `GetTileByLatLonQueryValidatorTests` — ≥ 35 methods across the 4 endpoints) + 4 blackbox (BT-28..BT-31 with ≥ 41 sub-cases) + 4 new contracts (`region-request.md` v1.0.0, `route-creation.md` v1.0.0, `tile-latlon.md` v1.0.0, `uav-tile-upload.md` v1.2.0 bump) + 4 probe scripts | 41/41 in-scope (AZ-808 AC-1..AC-8, AZ-809 AC-1..AC-8, AZ-810 AC-1..AC-9, AZ-811 AC-1..AC-9, AZ-812 AC-1..AC-6); 8 ACs are `◐ doc-verified at Step 13` (per-endpoint OpenAPI / system-flows updates) + 2 advisory non-tested (AZ-809 AC-9, AC-10 — naming consistency surfaced for parent-suite). AZ-810 AC-9 (no AZ-488 regression) verified after the AZ-810 test-data coord-clamp fix (commit `b763da3`) — the original "traced by source" verification was a false-PASS; the green full-suite re-run is the binding evidence. | — |
| Cycle 9 — AZ-1074 + AZ-1075 gRPC RouteTileDelivery (integration + unit + blackbox) | 1 integration file (`RouteTileDeliveryGrpcTests`) + orchestrator unit tests + 1 blackbox (BT-32 with 6 sub-cases) + `SatelliteProvider.GrpcContracts` | 7/7 (AZ-1074 AC-1..AC-4, AZ-1075 AC-1..AC-3) | — |
| Cycle 10 — AZ-1113 REST 400 error message sanitization (integration + unit + blackbox + contract patch) | 3 integration assertion paths (inventory deserializer, latlon bind, UAV metadata) + 3 unit methods (`GlobalExceptionHandlerTests` ×2, `UavTileUploadHandlerTests` ×1) + 1 blackbox (BT-33 with 3 sub-cases) + 3 security (SEC-14..SEC-16) + `error-shape.md` v1.0.1 patch | 5/5 in-scope (AZ-1113 AC-1..AC-5) | — |
| **Total** | **170** | **121/121 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 10 prior-cycle ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8); 2 advisory non-tested (cycle-8 AZ-809 AC-9/AC-10)** | **8/8 (100%)** |

**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 introduces no new HTTP route or wire-protocol change beyond the optional `metadata.flightId` field on the existing `POST /api/satellite/upload`. The 4 new BT scenarios (BT-19..BT-22) describe the new observable behaviors (multi-flight coexistence, deterministic id+content_sha256 on re-upload, float-rounded collapse, migration shape) without inventing a new endpoint surface.
- The cross-repo UUIDv5 namespace constant (`5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c`) is pinned in `SatelliteProvider.Common/Utils/Uuidv5.cs` and verified C#-side by 10 Python-generated reference vectors in `Uuidv5Tests`. The Python side runtime check is owned by sibling workspace `gps-denied-onboard` (AZ-304) — this matrix records the C# side as `✓` and the Python side as `◐` (out-of-workspace).

**Coverage shape notes (Cycle 5 — AZ-504 perf-script fix):**
- AZ-504 is a one-line harness bug fix; ACs are verified by a standalone shell harness (4 cases under `set -e -o pipefail`) embedded in `batch_01_cycle5_report.md`, not by the normal unit / integration suite. There is no production code path to add a test against — the bug is entirely in `scripts/run-performance-tests.sh`.
- AC-3 (PT-08 prints summary in full default-parameter run) and AC-4 (cycle-3 perf-harness leftover deleted on green full run) are gated at autodev Step 15 (Performance Test). The matrix marks them `◐ gate at Step 15` rather than `✓` until that step runs.

**Coverage shape notes (Cycle 2):**
- AZ-487 AC-7 (Swagger UI Authorize) is verified programmatically (`SwaggerDocument_AdvertisesBearerSecurityScheme`) rather than via a real UI flow; marked `◐ doc-verified`. The end-to-end browser-UI Authorize-button check remains a manual smoke before deploy.
- AZ-487 perf NFR (< 1 ms JWT overhead) remains `◐ recorded`; not separately gated. AZ-488 perf NFR (PT-08) moved from `◐ recorded (Deferred)` to `✓` for batch p95 — see PT-08 row above. AZ-484 perf NFR (PT-07) moved from `◐ recorded` to `✓` — see PT-07 row above. The harness work landed in AZ-492 (cycle 3) along with the `Authorization: Bearer …` attach that AZ-487 silently broke for the perf script.

**Coverage shape notes (Cycle 4 — AZ-500 .NET 8 → .NET 10 migration):**
- All 8 AZ-500 ACs are infrastructure-level (TFM/SDK pin/Docker base/package version/build/test-suite/doc) and are verified by re-running the **existing** test suite, the build pipeline, and `grep` over manifests. **No new test cases were added** — the contract being tested is "the previous 78 tests still pass on the new toolchain", which Step 11 confirmed (271 unit + integration green). Total counts above are unchanged.
- 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.
- 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.

**Coverage shape notes (Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename):**
- Cycle 7 is the **first** application of the AZ-795 shared validation infrastructure (FluentValidation 12.0.0 + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + camelCase naming policy). The infra is exercised end-to-end by every AZ-796 sub-case — the matrix records `AZ-795 (epic)` as `✓` because the infra is in place and the first concrete child (AZ-796) demonstrates it functions correctly. Sibling per-endpoint child tasks (other public-facing JSON endpoints) will land under AZ-795 in future cycles and each will get its own AC row at that time.
- AZ-794 (z/x/y rename) and AZ-796 (strict validation) shipped in the same commit (`865dfdb`). The matrix surfaces this coupling at AZ-794 AC-2 / AZ-796 AC-1 sub-case `9c` — the BT-27 sub-case that POSTs the legacy `{"tileZoom","tileX","tileY"}` payload now returns HTTP 400 with `errors[…]` naming `tileZoom`, proving (a) AZ-794's rename is observable on the wire and (b) AZ-795's `UnmappedMemberHandling.Disallow` catches the old names instead of silently coercing to `(0,0,0)`. The same sub-case carries the AZ-777 Phase 1 reproducer body verbatim — that exact request now fails-fast, closing the original discovery loop.
- AZ-794 has no perf, security, or resilience NFRs distinct from AZ-505's. Wire size is reduced ~3× on field names (per AZ-794 spec); not separately measured because the AZ-505 AC-4 p95 budget (1000 ms / 2500 tiles, measured 66 ms in cycle 6) already absorbs the rename with margin. Cycle 7 Step 11 reran the full integration suite (311 unit + integration) green; the AZ-505 perf budget is re-measured at Step 15 of cycle 7 per the existing `◐ gate at Step 15` rows.
- AZ-796 AC-3 (validator unit-tested) and AC-4 (integration-tested) each specify a minimum count (≥ 9 unit, ≥ 10 integration). Cycle 7 delivered 16 + 16 = 32 — comfortably over the floor — covering every `RuleFor(…)` in `InventoryRequestValidator` and `TileCoordValidator` plus the JSON-deserializer-level rules (`JsonRequired`, `UnmappedMemberHandling.Disallow`, type mismatch) that don't reach the validator. The split is documented in BT-27 §Notes.
- Doc-only ACs (AZ-794 AC-3, AZ-796 AC-5 — OpenAPI / Swagger spec accuracy) are marked `◐ doc-verified at Step 13` because they require inspection of the generated `/swagger/v1/swagger.json` during the Update Docs step. Cycle 7's Swashbuckle output reflects the rename + ranges automatically via the DTOs' `[JsonRequired]` and the validator's `RuleFor` constraints — no manual OpenAPI XML doc edits were needed. Step 13 will verify against the running container's swagger document.
- AZ-505 AC-6's existing row (cycle 6 — "Request validation — 400 on both populated, 400 on neither, 400 on > 5000 entries, 401 on anonymous") remains accurate. Its 4 cases overlap with AZ-796 AC-1 sub-cases 2a, 2b, 4, and the anonymous case (also SEC-05). Both rows are kept per cycle-update rule 4 ("Preserve existing traceability IDs"); the duplication is by design — AZ-505 AC-6 was the cycle-6 contract (status-code-only), AZ-796 AC-1 is the cycle-7 contract (status code + ProblemDetails shape + field-path errors). The cycle-7 row is the binding one going forward; the cycle-6 row stays as historical record.
- Cycle-update rule check: no NFR conflicts. The 5000-entry cap is reaffirmed (matches AZ-505); the supported zoom range 0..22 is reaffirmed (matches `tile-inventory.md` Inv-7); the error shape contract is **new** (`error-shape.md` v1.0.0) — but no prior cycle declared a different error shape, so this is greenfield content, not a conflict.
- Step 10 artifact gap (cycle 7): no `implementation_report_*_cycle7.md` was produced in `_docs/03_implementation/`. The actual implementation evidence lives in commits `dceaddc` (cycle 7 task adoption) + `865dfdb` (cycle 7 Step 10 implementation), in the state file's `detail` field (which recorded the test-run outcome), and in the new test artifacts themselves (`InventoryRequestValidator.cs`, `InventoryRequestValidatorTests.cs`, `TileInventoryValidationTests.cs`, `ProblemDetailsAssertions.cs`, `error-shape.md` v1.0.0). This artifact gap is recorded here for cycle 7 retrospective follow-up — the matrix itself is unaffected because cycle-update mode's source-of-truth is the task specs in `_docs/02_tasks/done/`, not the implementation report.

**Coverage shape notes (Cycle 8 — AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 strict validation sweep + region OSM rename):**
- Cycle 8 completes the AZ-795 epic's per-endpoint rollout — every public-facing endpoint now goes through the shared validation infra. AZ-795's `AZ-795 (epic)` row from cycle 7 remains `✓`; cycle 8 adds 4 endpoint-scoped per-AC rows (AZ-808, AZ-809, AZ-810, AZ-811) plus the AZ-812 region-rename rows that ride the AZ-795 `UnmappedMemberHandling.Disallow` infra to make the old field names fail-fast (mirroring cycle 7's AZ-794 / AZ-796 coupling).
- AZ-810 introduced a **new** validation enforcement shape — the `multipart/form-data` envelope — because `POST /api/satellite/upload` is the only endpoint that can't use the generic `ValidationEndpointFilter<T>`. The bespoke `UavUploadValidationFilter` composes three layers (deserializer, FluentValidation, envelope cross-field) with a different error-key convention (`errors["metadata"]` for deserializer-level failures vs `errors["metadata.items[i].field"]` for FluentValidation-layer failures). This is documented in BT-30 §Notes and `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 §Validation Rules so future multipart endpoints can reuse the pattern.
- AZ-811 introduced a **new** generic infra piece — `UnknownQueryParameterEndpointFilter` (rule 4 — the parallel of `UnmappedMemberHandling.Disallow` for query strings). Documented in `_docs/02_document/modules/api_program.md` per AZ-811 AC-9. The next query-param endpoint can reuse it without reinventing the unknown-key rejection logic.
- AZ-808 + AZ-812 shipped in the same cycle. The AZ-812 OSM rename (`Latitude/Longitude` → `Lat/Lon`) was coordinated with AZ-808's validator authoring so the validator was never written against the old names (per AZ-812 AC-6 coordination). `region-request.md` is published as v1.0.0 (not v1.0.0→v2.0.0 bump) with both AZ-808 (validation rules) and AZ-812 (`lat`/`lon` field names) in the Change Log.
- BT-N01 and BT-N02 (legacy negative scenarios for `GET /api/satellite/tiles/latlon` that loosely asserted "HTTP 4xx") are NOT rewritten — they remain as historical record. BT-31 sub-cases 1, 2, 3 supersede them with strict assertions (HTTP 400 + named `errors` key). Both rows are kept per cycle-update rule 4 ("Preserve existing traceability IDs"); the cycle-8 row is the binding one going forward.
- AZ-809 ACs 9 + 10 are **advisory** (surfaced for parent-suite team decision, not implemented or tested this cycle). Matrix marks them `◐ advisory (not tested)`. They're recorded so the next cycle / parent-suite review sees them without having to re-discover them from the task spec. AC-9: `RequestRegionRequest.sizeMeters` vs `CreateRouteRequest.regionSizeMeters` naming inconsistency. AC-10: input `points: [{lat, lon}]` vs output `points: [{latitude, longitude}]` round-trip asymmetry on the route endpoint. Either keep + document, or harmonize in a follow-up MAJOR contract bump for both — parent-suite team's call.
- AZ-810 AC-9 (no AZ-488 regression) has a **process annotation**: cycle 8's batch_04 report originally claimed AC-9 "verified by tracing each AZ-488 test payload's metadata shape against the new rules" without running the integration suite. That verification was a false-PASS — the suite was actually red on the AZ-488 happy path because `UavUploadTests.NextTestCoordinate()` produced lat > 90° (a pre-existing latent bug masked by the absence of any validator before AZ-810). The bug was fixed by clamping the test-data generator to OSM-valid ranges in commit `b763da3` and AC-9 is now bound to the green full-suite re-run, not to source tracing. Process lesson recorded in `_docs/LESSONS.md` (2026-05-23).
- Cycle-update rule check: no NFR conflicts. Range bounds (`lat ∈ [-90, 90]`, `lon ∈ [-180, 180]`, `zoom ∈ [0, 22]`, `tileSizeMeters > 0`) are reaffirmed across all 4 endpoints — they were never previously contested. The error-shape contract (`error-shape.md` v1.0.0 from cycle 7) is reused unchanged.

| AZ-1074 AC-1 | Happy path streams tiles — `RouteManifest` + ≥1 `TileChunk` + `DeliveryComplete` | BT-32 pos (blackbox); `RouteTileDeliveryGrpcTests.RunHappyPath` (integration) | ✓ |
| AZ-1074 AC-2 | Cached tiles served without redundant Google Maps download | `RouteTileDeliveryOrchestratorTests.DeliverAsync_CachedTileOnDisk_EmitsBatchWithoutDownload` (unit); BT-32 pos reuses compose volume cache (integration) | ✓ |
| AZ-1074 AC-3 | Invalid route / coordinates rejected with `INVALID_ARGUMENT` | BT-32 sub-cases 1–3 (blackbox); `RouteTileDeliveryGrpcTests.RunInvalidRequests` (integration) | ✓ |
| AZ-1074 AC-4 | Slow consumer — tile bytes not corrupted | BT-32 sub-case 4 (blackbox); `RouteTileDeliveryGrpcTests.RunBackpressureSafe` (integration) | ✓ |
| AZ-1075 AC-1 | gRPC happy-path passes in docker-compose full run | Full `scripts/run-tests.sh --full` / `docker-compose.tests.yml` (cycle 9 Step 11 — passed) | ✓ |
| AZ-1075 AC-2 | Each invalid variant returns expected gRPC status | BT-32 sub-cases 1–3; `RouteTileDeliveryGrpcTests.RunInvalidRequests` | ✓ |
| AZ-1075 AC-3 | REST and gRPC tile metadata consistent for same route | BT-32 sub-case 5; `RouteTileDeliveryGrpcTests.RunRestConsistency` | ✓ |
| AZ-1113 AC-1 | `GlobalExceptionHandler` + inner `JsonException` → static `errors[]` message (no `.NET` type leak) | SEC-14, BT-33 sub-case 1 (blackbox); `GlobalExceptionHandlerTests.TryHandleAsync_DeserializationFailure_WritesValidationProblemDetailsWithJsonPath_AZ795` (unit); `TileInventoryValidationTests.UnknownNestedField_Returns400` + region/route/create-route validation tests asserting `"The field value is invalid."` (integration) | ✓ |
| AZ-1113 AC-2 | `BadHttpRequestException` without `JsonException` → static `detail` | SEC-15, BT-33 sub-case 2 (blackbox); `GlobalExceptionHandlerTests.TryHandleAsync_BadHttpRequestExceptionWithoutJson_UsesStaticDetail` (unit); `GetTileByLatLonValidationTests.LatTypeMismatch_Returns400` (integration) | ✓ |
| AZ-1113 AC-3 | `UavUploadValidationFilter` metadata parse → static `errors["metadata"]` | SEC-16, BT-33 sub-case 3 (blackbox); `UavUploadValidationTests.MetadataNotAnObject_Returns400` (integration; asserts no `System.` in body) | ✓ |
| AZ-1113 AC-4 | `UavTileUploadHandler` defense-in-depth metadata parse → static envelope error | `UavTileUploadHandlerTests.HandleAsync_InvalidMetadataJson_ReturnsEnvelopeError` (unit) | ✓ |
| AZ-1113 AC-5 | `error-shape.md` v1.0.1 Information Disclosure section documents static strings | doc-state AC; verified at Step 13 (Update Docs) | ✓ |

**Coverage shape notes (Cycle 9 — AZ-1074 + AZ-1075 gRPC RouteTileDelivery):**
- Cycle 9 adds the first gRPC blackbox surface alongside the existing REST suite. BT-32 is the binding blackbox spec; integration coverage lives in `RouteTileDeliveryGrpcTests` wired into both smoke and full suites via `Program.cs`.
- Proto source of truth moved to `SatelliteProvider.GrpcContracts/tile_provision.proto` (GrpcServices=Both); contract doc at `_docs/02_document/contracts/c11_tilemanager/tile_provision_grpc.md`.
- Cycle 9 Step 11 initially failed integration startup due to host port 5433 conflict with sibling project `fleet-viewer-dev-db`. Fixed by making `docker-compose.tests.yml` self-contained (no host port publishing — compose-internal networking only) and pointing `scripts/run-tests.sh` at that file alone for integration runs. Unit count is now 448 (includes orchestrator + gRPC validation tests).
- No perf / security NFRs declared in AZ-1074/1075 task specs beyond existing JWT-on-gRPC-metadata (inherits AZ-487/494 invariants). Load testing explicitly excluded.
- Cycle-update rule check: no NFR conflicts.

**Coverage shape notes (Cycle 10 — AZ-1113 REST 400 error message sanitization):**
- Cycle 10 is a **patch-level** contract tightening (`error-shape.md` v1.0.0 → v1.0.1) — no new HTTP routes, no new validation rules, no perf/security harness changes. The observable change is message *content* on existing 400 paths only; field paths and HTTP status codes are unchanged (AZ-1113 Compatibility NFR).
- Three call sites sanitized: `GlobalExceptionHandler` (JSON deserializer + non-JSON bind), `UavUploadValidationFilter`, `UavTileUploadHandler` (defense-in-depth). gRPC `DeliveryError` path was already sanitized in cycle 9 — out of scope per task spec.
- BT-33 + SEC-14..SEC-16 are deliberately **cross-cutting** rather than per-endpoint duplicates of BT-27..BT-31 — cycle-update rule 4 preserves existing traceability IDs; the cycle-8 rows remain the binding functional specs, cycle-10 rows add the message-content contract on top.
- Integration tests that previously asserted substring matches on raw `JsonException.Message` were updated to assert the static strings (`RegionRequestValidationTests`, `CreateRouteValidationTests`, `TileInventoryValidationTests`, `UavUploadValidationTests`). No new integration test *files* — assertion tightening on existing failure methods.
- AZ-1113 AC-5 is doc-only (`error-shape.md` §Information disclosure) — verified at Step 13 (`api_program.md` + contract doc).
- Step 11 evidence: smoke PASS 450/450 unit + integration EXIT:0 (~2.5m) per state file; full perf gate unchanged (REST-only PT scenarios still apply).
- Cycle-update rule check: no NFR conflicts. Inv-5 scope expands from 5xx-only (AZ-353) to include deserializer/binding 4xx — not a conflict because no prior cycle declared the opposite for 4xx message content.
