Files
satellite-provider/_docs/02_document/tests/traceability-matrix.md
T
Oleksandr Bezdieniezhnykh bc04ba7f99 [AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-15 sync (test-spec / docs / security / perf)
Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule
validation surface and 12 cycle-7 AC rows + Coverage Summary
update to traceability-matrix.md.

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 11:24:27 +03:00

212 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 + 240244 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 | ✓ |
## 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 | 11 | 9 (AZ-487 AC-1..AC-7, AZ-488 AC-6, leak-hygiene NFR) | 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`. | — |
| **Total** | **126** | **75/75 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 2 cycle-7 ACs doc-verified at Step 13** | **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.