# Cycle 8 — Documentation Ripple Log **Cycle**: 8 (AZ-808 region-request validation + AZ-809 route-creation validation + AZ-810 UAV upload metadata validation + AZ-811 GET tiles/latlon validation + AZ-812 Region OSM rename) **Generated by**: `/document` skill (task mode) during autodev Step 13 **Resolution method**: `Grep --type cs` against every new or changed symbol introduced by the five tasks. C# `using`-based import analysis on `RequestRegionRequest` (renamed `Lat`/`Lon`), `UavTileMetadata` + `UavTileBatchMetadataPayload` (`[JsonRequired]`), `CreateRouteRequest` + `RoutePoint` + `GeofencePolygon` + `Geofences` + `GeoPoint` (`[JsonRequired]`), the four new validator classes, the two new envelope filters (`UavUploadValidationFilter`, `RejectUnknownQueryParamsEndpointFilter`), and the new query DTO `GetTileByLatLonQuery`. No static-analyzer used — the new surface lives almost entirely behind `Program.cs` + the four per-endpoint test files, so the literal usage scan is exhaustive. ## Directly-changed source files (cycle 8) - `SatelliteProvider.Common/DTO/RequestRegionRequest.cs` (AZ-812, modified) — C# properties renamed `Latitude/Longitude` → `Lat/Lon` with `[JsonPropertyName("lat")]` / `[JsonPropertyName("lon")]`. AZ-808 added `[JsonRequired]` to `Id` + the two coordinate axes + `SizeMeters` + `ZoomLevel` + `StitchTiles` so partial bodies are rejected at the deserializer layer. - `SatelliteProvider.Common/DTO/UavTileMetadata.cs` (AZ-810, modified) — `[JsonRequired]` added to `Latitude`, `Longitude`, `TileZoom`, `TileSizeMeters`, `CapturedAt`. `FlightId` intentionally NOT marked required (AZ-503 anonymous-flight semantics require `null` to be valid). `UavTileBatchMetadataPayload.Items` also marked `[JsonRequired]`. - `SatelliteProvider.Common/DTO/RoutePoint.cs` (AZ-809, modified) — `[JsonRequired]` on `Latitude` + `Longitude`; `[JsonPropertyName("lat")]` / `[JsonPropertyName("lon")]` confirm the wire shape pre-rename (`RoutePoint` was already using `lat`/`lon`, only `RequestRegionRequest` was the OSM-rename target). - `SatelliteProvider.Common/DTO/CreateRouteRequest.cs` (AZ-809, modified) — `[JsonRequired]` on every non-optional field (`Id`, `Name`, `RegionSizeMeters`, `ZoomLevel`, `Points`, `RequestMaps`, `CreateTilesZip`); `Description` and `Geofences` left optional. - `SatelliteProvider.Common/DTO/GeofencePolygon.cs` (AZ-809, modified) — `[JsonRequired]` on `NorthWest` + `SouthEast` so partial polygons fail at the deserializer. - `SatelliteProvider.Common/DTO/Geofences.cs` (AZ-809, modified) — `[JsonRequired]` on `Polygons` so `geofences: {}` is rejected. - `SatelliteProvider.Common/DTO/GeoPoint.cs` (AZ-809, modified) — `[JsonRequired]` on both axes for the geofence-corner case. - `SatelliteProvider.Api/Program.cs` (AZ-808/809/810/811/812, modified) — - `.WithValidation()` on `MapPost("/api/satellite/request", …)` (AZ-808). - `.WithValidation()` on `MapPost("/api/satellite/route", …)` (AZ-809). - `.AddEndpointFilter()` on `MapPost("/api/satellite/upload", …)` (AZ-810) — bespoke multipart filter, not the generic `WithValidation()`. - `.AddEndpointFilter(new RejectUnknownQueryParamsEndpointFilter("lat", "lon", "zoom"))` + `.WithValidation()` on `MapGet("/api/satellite/tiles/latlon", …)` (AZ-811) — two-filter chain so unknown-key rejection precedes range checks. - `AddTransient()` registration (AZ-810 — the filter has injected dependencies). - The `ParameterDescriptionFilter` Swagger op filter trimmed the obsolete `Latitude`/`Longitude`/`ZoomLevel` query-param annotations (AZ-811 cleanup; lat/lon/zoom remain). - `SatelliteProvider.Api/Validators/RegionRequestValidator.cs` (AZ-808, **new**) — `AbstractValidator` with rules for non-zero `id`, `lat`/`lon` ranges, `sizeMeters` ∈ \[100, 10000\], `zoomLevel` ∈ \[0, 22\]. - `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs` (AZ-809, **new**) — root validator chaining `RuleForEach(req => req.Points).SetValidator(new RoutePointValidator())` for per-point checks, `RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator()).OverridePropertyName("geofences.polygons")` for per-polygon checks, plus `Must(req => !(req.CreateTilesZip && !req.RequestMaps)).WithName("createTilesZip")` for the cross-field rule. Note the `OverridePropertyName` is required because FluentValidation's default name policy drops the parent on deep `req.Geofences!.Polygons` expressions. - `SatelliteProvider.Api/Validators/RoutePointValidator.cs` (AZ-809, **new**) — per-point lat/lon range rules with `OverridePropertyName("lat"/"lon")` so error keys match the wire format. - `SatelliteProvider.Api/Validators/GeofencePolygonValidator.cs` (AZ-809, **new**) — cross-field invariants `NW.Lat > SE.Lat` AND `NW.Lon < SE.Lon` for axis-aligned bounding boxes. - `SatelliteProvider.Api/Validators/UavTileBatchMetadataPayloadValidator.cs` (AZ-810, **new**) — `items` count rules (non-null, non-empty, ≤ `UavQualityConfig.MaxBatchSize`) + `RuleForEach(p => p.Items).SetValidator(new UavTileMetadataValidator(...))`. - `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs` (AZ-810, **new**) — per-item rules: `latitude` ∈ \[-90, 90\], `longitude` ∈ \[-180, 180\], `tileZoom` ∈ \[0, 22\], `tileSizeMeters` > 0, `capturedAt` within `\[now - MaxAgeDays, now + CapturedAtFutureSkewSeconds\]`. Uses an injectable `TimeProvider` (defaults to `TimeProvider.System`). - `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs` (AZ-810, **new**) — `IEndpointFilter` for `POST /api/satellite/upload`. Reads the `metadata` form field, deserializes via the strict global `JsonSerializerOptions`, runs `IValidator` from DI, and enforces the envelope cross-field rule `items.Count == files.Count`. Error-map keys prefixed with `metadata.` so paths surface to the caller as `errors["metadata.items[0].latitude"]`. - `SatelliteProvider.Api/Validators/GetTileByLatLonQueryValidator.cs` (AZ-811, **new**) — `AbstractValidator` with `lat`/`lon`/`zoom` rules using `Cascade(CascadeMode.Stop) → NotNull → InclusiveBetween` so missing-param surfaces only as `"\`\` is required."`. - `SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs` (AZ-811, **new**) — generic envelope filter parameterized by an allowed-keys set; rejects unknown query-string parameters with RFC 7807 `ValidationProblemDetails`. **New shared infrastructure** designed for reuse by any future query-param endpoint (AZ-811 AC-9). - `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs` (AZ-811, **new**) — `record GetTileByLatLonQuery(double? Lat, double? Lon, int? Zoom)` with `[FromQuery(Name="lat"|"lon"|"zoom")]` on each property. Bound via `[AsParameters]` on the handler. Nullable on purpose — see api_program.md commentary. - `_docs/02_document/contracts/api/region-request.md` (AZ-808 + AZ-812, **new**) — v1.0.0 wire-format contract for `POST /api/satellite/request`. Published directly with `lat`/`lon` per AZ-812 AC-6 coordination (no v2.0.0 bump needed since AZ-808 + AZ-812 shipped same-cycle). - `_docs/02_document/contracts/api/route-creation.md` (AZ-809, **new**) — v1.0.0 wire-format contract for `POST /api/satellite/route` covering all 14 validation rules, the nested per-point / per-polygon structure, and the `createTilesZip ⇒ requestMaps` cross-field invariant. Carries advisory notes for AZ-809 AC-9 (`sizeMeters` vs `regionSizeMeters` naming inconsistency) and AC-10 (input/output point-shape asymmetry). - `_docs/02_document/contracts/api/tile-latlon.md` (AZ-811, **new**) — v1.0.0 wire-format contract for `GET /api/satellite/tiles/latlon` covering the 5 validation rules + the novel unknown-query-param rejection. Documents the rename `?Latitude=&Longitude=&ZoomLevel=` → `?lat=&lon=&zoom=`. - `_docs/02_document/contracts/api/uav-tile-upload.md` (AZ-810, modified) — bumped to v1.2.0; new "Validation Rules" section covering the three-layer enforcement (deserializer, FluentValidation, envelope cross-field) and the `errors["metadata.…"]` key convention. Change Log entry names AZ-810. - `_docs/02_document/modules/api_program.md` (AZ-808/809/810/811/812, modified) — endpoint table entries for the 4 endpoints bumped to credit cycle 8 and reference the new contract docs. New `Api/Validators` section row for each cycle-8 validator. New `Api/DTOs` section for `GetTileByLatLonQuery`. DI Registration item 14 lists every cycle-8 validator. New item documenting the `UavUploadValidationFilter` AddTransient + AddEndpointFilter wiring. - `_docs/02_document/modules/common_dtos.md` (AZ-808/809/810/812, modified) — `RequestRegionRequest` renamed + JsonPropertyName documented; `RoutePoint`, `CreateRouteRequest`, `GeofencePolygon`, `Geofences`, `GeoPoint`, `UavTileMetadata`, `UavTileBatchMetadataPayload` all carry `[JsonRequired]` annotations explained; input/output `lat`/`latitude` asymmetry on the route endpoint surfaced. - `_docs/02_document/system-flows.md` (AZ-808/809/811, modified) — F1 (single-tile download) updated to reference `tile-latlon.md` + the unknown-query-param filter. F2 (region request) updated to reference `region-request.md` + the validator. F4 (route creation) updated to reference `route-creation.md` + the cross-field rules. - `_docs/02_document/architecture.md` (cycle 8, modified by Step 13) — (a) the contracts inventory line bumped to mention `uav-tile-upload.md` v1.2.0 + the four new cycle-8 contracts (`region-request.md`, `route-creation.md`, `tile-latlon.md`, `error-shape.md`); (b) new architectural principle "Strict wire-format validation at the API edge (AZ-795 epic, completed across cycles 7-8)" describing the two-layer enforcement and the no-handler-without-validation rule. - `SatelliteProvider.Tests/Validators/RegionRequestValidatorTests.cs` (AZ-808, **new**) — unit tests against each `RuleFor` chain. - `SatelliteProvider.Tests/Validators/CreateRouteRequestValidatorTests.cs` + `RoutePointValidatorTests.cs` + `GeofencePolygonValidatorTests.cs` (AZ-809, **new**) — ≥ 13 unit-test methods across the three validators. - `SatelliteProvider.Tests/Validators/UavTileMetadataValidatorTests.cs` + `UavTileBatchMetadataPayloadValidatorTests.cs` (AZ-810, **new**) — ≥ 11 unit-test methods covering each rule (incl. `TimeProvider` injection for freshness). - `SatelliteProvider.Tests/Validators/GetTileByLatLonQueryValidatorTests.cs` (AZ-811, **new**) — ≥ 3 unit-test methods. - `SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs` + `CreateRouteValidationTests.cs` + `UavUploadValidationTests.cs` + `GetTileByLatLonValidationTests.cs` (AZ-808/809/810/811, **new**) — ≥ 45 failure methods + 4 happy paths total; every test uses `ProblemDetailsAssertions` from AZ-795. - `SatelliteProvider.IntegrationTests/UavUploadTests.cs` (AZ-810 fallout, modified) — `NextTestCoordinate()` clamped to lat ∈ \[50, 70), lon ∈ \[10, 40) via modulo arithmetic. Pre-AZ-810 the seed `(Ticks/TicksPerSecond) % 1_000_000` produced lat > 90° which was silently accepted by the lenient pre-cycle-8 deserializer; the new AZ-810 validator (correctly) rejects it. This is the test-data bug that exposed the AC-9 false-PASS (see `_docs/LESSONS.md` 2026-05-23 entry). - `SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs` (AZ-810, modified — same fix) — `NextTestCoordinate()` clamped to lat ∈ \[-70, -50), lon ∈ \[-40, -10) (non-overlapping with `UavUploadTests` to avoid per-source UNIQUE-index collisions when both suites run against the same DB). - `scripts/probe_region_validation.sh` + `probe_route_validation.sh` + `probe_upload_validation.sh` + `probe_latlon_validation.sh` (AZ-808/809/810/811, **new**) — manual probe scripts modelled on `probe_inventory_validation.sh`. ## Importer scan results | Symbol | Importer count | Importer files | Component touched | |--------|----------------|----------------|-------------------| | `RequestRegionRequest.Lat` / `.Lon` (renamed properties; wire names unchanged at `lat`/`lon`) | 4 | `Program.cs` (request mapping), `RegionService.cs` (handler — uses `.Lat`/`.Lon` to build the queue message), `RegionRequestTests.cs`, `RegionRequestValidationTests.cs` | WebApi, RegionProcessing service, Tests | | `[JsonRequired]` on every cycle-8 DTO axis | n/a | enforced at runtime by `System.Text.Json` + caught by `GlobalExceptionHandler` (no compile-time consumer beyond the deserializer) | WebApi (deserializer + handler) | | `RegionRequestValidator` | 3 | `Program.cs` (assembly-scan registration), `RegionRequestValidatorTests.cs`, `RegionRequestValidationTests.cs` (indirect via running API) | WebApi (production), Tests (unit + integration) | | `CreateRouteRequestValidator` + `RoutePointValidator` + `GeofencePolygonValidator` | 4 | `Program.cs`, 3 unit-test files, `CreateRouteValidationTests.cs` (indirect) | WebApi (production), Tests (unit + integration) | | `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator` | 3 | `Program.cs`, 2 unit-test files, `UavUploadValidationTests.cs` (indirect) | WebApi (production), Tests (unit + integration) | | `GetTileByLatLonQueryValidator` | 3 | `Program.cs`, `GetTileByLatLonQueryValidatorTests.cs`, `GetTileByLatLonValidationTests.cs` (indirect) | WebApi (production), Tests (unit + integration) | | `UavUploadValidationFilter` | 2 | `Program.cs` (DI + endpoint filter wiring), `UavUploadValidationTests.cs` (indirect) | WebApi | | `RejectUnknownQueryParamsEndpointFilter` | 1 (current) + N-future | `Program.cs` (`MapGet("/api/satellite/tiles/latlon", …).AddEndpointFilter(new RejectUnknownQueryParamsEndpointFilter(...))`); designed to be reused by every future query-param endpoint per AZ-811 AC-9 | WebApi | | `GetTileByLatLonQuery` | 2 | `Program.cs` (handler signature `[AsParameters] GetTileByLatLonQuery`), `GetTileByLatLonQueryValidatorTests.cs` | WebApi (production), Tests (unit) | | `RoutePoint.Latitude/Longitude` + `[JsonRequired]` | 4 | `RouteService.cs` (handler), `RouteCreationTests.cs`, `CreateRouteValidationTests.cs`, `RoutePointValidatorTests.cs` | WebApi, RouteManagement service, Tests | | `CreateRouteRequest.*` + `[JsonRequired]` | 4 | `Program.cs`, `RouteService.cs`, `RouteCreationTests.cs`, `CreateRouteValidationTests.cs` | WebApi, RouteManagement service, Tests | | `GeofencePolygon`/`Geofences`/`GeoPoint` + `[JsonRequired]` | 5 | `CreateRouteRequest.cs`, `RouteService.cs` (point-in-polygon geofence filtering), `GeofencePolygonValidatorTests.cs`, `CreateRouteValidationTests.cs`, `Json` deserializer | WebApi, RouteManagement service, Tests | ## Doc refresh decisions All importers land inside components that either received targeted updates during Step 10 (Implement) or were verified-clean during this Step 13: - **WebApi (`Program.cs`)** — `_docs/02_document/modules/api_program.md` updated during the implementation phase with: (a) endpoint table entries for the 4 cycle-8 endpoints crediting their respective tasks + contract docs, (b) new `Api/Validators` section rows for every cycle-8 validator + envelope filter, (c) new `Api/DTOs` section for `GetTileByLatLonQuery`, (d) DI Registration item 14 listing every cycle-8 validator, (e) DI Registration entry for the `UavUploadValidationFilter` AddTransient + AddEndpointFilter wiring. Verified during Step 13 — no further changes needed. - **Common (DTOs)** — `_docs/02_document/modules/common_dtos.md` updated with every modified DTO carrying its `[JsonRequired]` annotations explained, the AZ-812 `Lat/Lon` rename + JsonPropertyName attributes, and the route-endpoint input/output naming asymmetry caveat. Verified during Step 13 — no further changes needed. - **RegionProcessing (`RegionService.cs`)** — no module doc update needed; the handler's behavior is unchanged (it still reads `.Lat`/`.Lon` from the request DTO to build the queue message — only the property names changed, not the values or the queue contract). The internal `RegionRequest` queue message remains on `Latitude`/`Longitude` per design (intentionally kept, see `common_dtos.md` line 43 commentary). - **RouteManagement (`RouteService.cs`)** — no module doc update needed; the handler's behavior is unchanged. The `[JsonRequired]` annotations only affect the deserializer layer — once a payload passes, the handler sees the same shape it always did. - **WebApi (`GlobalExceptionHandler.cs`)** — unchanged from cycle 7. The handler is now exercised by 4 more endpoints' deserializer-layer failures (missing `[JsonRequired]` axes, unknown fields, type mismatches) but the implementation is identical. - **TileDownloader / DataAccess / DataAccess migrations** — not touched by cycle 8. - **Architecture** — `architecture.md` updated during Step 13 with: (a) contracts inventory line bumped to mention `uav-tile-upload.md` v1.2.0 + the four new cycle-8 contracts, (b) new "Strict wire-format validation at the API edge" architectural principle describing the two-layer enforcement and the no-handler-without-validation rule. - **System flows** — `system-flows.md` F1/F2/F4 updated during the implementation phase to credit cycle 8 and reference the new contract docs + error-shape contract. F6 (status query) + F8 (tile inventory bulk lookup) untouched — cycle 8 didn't change those endpoints. - **Tests (unit + integration)** — `_docs/02_document/modules/tests_unit.md` and `tests_integration.md` are not strictly required to enumerate every new test file (cycle 7 didn't extend them past a "AZ-795 + AZ-796 — strict inventory validation" subsection). Cycle 8 keeps the same convention — the new test files are documented in the traceability matrix + test-spec sync (BT-28..BT-31) rather than re-listed per file in the module docs. - **Tests (blackbox + traceability)** — `tests/blackbox-tests.md` and `tests/traceability-matrix.md` updated during Step 12 (Test-Spec Sync): BT-28..BT-31 added + 41 AC rows added (AZ-808 AC-1..AC-8 + AZ-809 AC-1..AC-10 + AZ-810 AC-1..AC-9 + AZ-811 AC-1..AC-9 + AZ-812 AC-1..AC-6) + Coverage Summary refresh. ## No-ripple components These components were NOT touched by cycle-8 changes and require no doc update: - **DataAccess** — no schema, repository signature, or migration changes in cycle 8. The validation work is entirely at the API edge. - **TileDownloader** — not touched. The four cycle-8 endpoints either don't trigger tile downloads at all (`POST /api/satellite/request`, `POST /api/satellite/route`) or trigger them only after the validator has passed (`GET /api/satellite/tiles/latlon`, `POST /api/satellite/upload`). - **RegionProcessing background service** — not touched. The validator runs at the API edge before the request is enqueued. - **RouteManagement processing** — not touched for the same reason. ## Parse-failure / heuristic notes None — every symbol resolved via direct `Grep --type cs`. No fallback heuristic was needed. The cycle 8 surface is wider than cycle 7 (5 tasks vs 3) but still narrow architecturally — every change is a WebApi-layer concern + DTO annotations + per-endpoint test files. The shared infrastructure landed in cycle 7 (AZ-795); cycle 8 is the per-endpoint rollout. ## AZ-795 epic posture Cycle 8 completes the per-endpoint rollout of the AZ-795 strict-validation epic. After this cycle, **every public-facing JSON, multipart, and query-param endpoint** in the satellite-provider workspace goes through one of the three approved validation paths: 1. **JSON-body endpoints** — `WithValidation()` + `ValidationEndpointFilter` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`. Used by `POST /api/satellite/tiles/inventory` (AZ-796 cycle 7), `POST /api/satellite/request` (AZ-808 cycle 8), `POST /api/satellite/route` (AZ-809 cycle 8). 2. **Multipart endpoints** — bespoke `UavUploadValidationFilter` composing deserializer + FluentValidation + envelope cross-field. Used by `POST /api/satellite/upload` (AZ-810 cycle 8). 3. **Query-param endpoints** — `RejectUnknownQueryParamsEndpointFilter` + `WithValidation()`. Used by `GET /api/satellite/tiles/latlon` (AZ-811 cycle 8). The `architecture.md` § "Strict wire-format validation at the API edge" principle (added this Step 13) codifies this as a rule for future endpoints — there is no fourth approved path. The previously-open AZ-795 epic now has zero outstanding child tickets in this workspace; future endpoints will pick one of the three paths above and reuse the existing infrastructure without new shared-infra work. The endpoints NOT validated by the AZ-795 stack are: `GET /api/satellite/region/{id}` (path-only, framework-handled Guid binding — covered by the strict path binder), `GET /api/satellite/route/{id}` (same), `GET /api/satellite/tiles/mgrs` (stub returning empty — no input to validate), `GET /tiles/{z}/{x}/{y}` (path-only, framework-handled int binding — the strict path binder rejects malformed values; whether to range-check `z`/`x`/`y` against slippy-map bounds is a separate decision deferred to parent-suite team per AZ-811 Out of Scope). These exemptions are documented in `api_program.md` so future contributors know they're intentional, not omissions.