6 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh 62d6b8310a [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 retro + close
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Cycle 8 retrospective (cycle-end mode): 5 tickets shipped, 17 SP, 4
batches across 1 cycle theme (strict input validation for the 4
AZ-795 child endpoints + AZ-812 region API field rename).

Artifacts:
- _docs/06_metrics/retro_2026-05-23_cycle8.md
- _docs/06_metrics/structure_2026-05-23_cycle8.md (gap-filled; last
  structural snapshot was cycle 5)

Key cycle-8 findings (now in _docs/LESSONS.md ring buffer):
- Step-14 security-audit Medium findings under the small-fix
  threshold should be resolved in-cycle, not deferred (F-AZ809-1
  closed in commit 8fca6e0, ~30 min from discovery to fix).
- Retro recommendations ship end-to-end when they name concrete
  tickets/files + size as a coherent cycle theme (cycle 7 Action 3
  -> cycle 8 strict-validation slate, first end-to-end traceable
  cross-cycle improvement action in project history).
- Contract wire-format updates (new required field / rename) need a
  ripgrep probe across all consumer paths (perf script, probe
  scripts, README, deploy docs, OpenAPI examples) — partial syncs
  surface at Step 15 perf gate (PT-06 missed AZ-809 requestMaps +
  createTilesZip, fixed in commit 32bc5c1).

Carry-overs to cycle 9: track PT-07 cache-pollution false positive
(harness, not regression), reduce 3 cycles in a row of misleading
"PT-07 fails on warm/cold ratio" entries in the perf report.

Marks Step 17 completed; cycle 8 closed. Next /autodev invocation
starts cycle 9 from Step 0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:17:03 +03:00
Oleksandr Bezdieniezhnykh 32bc5c1e48 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 perf run
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
8/8 scenarios PASS within threshold. Cycle-8 strict-validation
overhead is below percentile resolution on every measured
endpoint.

PT-06 (route creation) required one in-cycle perf-script fix:
add requestMaps=false + createTilesZip=false to the body to
satisfy AZ-809's no-defaulting rule. The script had already
been updated for AZ-812's wire rename during cycle 8 but missed
AZ-809's newly required fields. Production code is correct; only
the perf probe was stale.

Report: _docs/06_metrics/perf_2026-05-23_cycle8.md. Trend vs
cycle 7 is flat within noise band on every scenario.

Known harness quirks (pre-existing, not cycle-8 regressions)
surfaced and documented for cleanup:
- PT-07 cross-run cache pollution (hard-coded base coords)
- PT-01 "cold" misnomer (tile cached on disk since cycle 5)
- PT-03 cached-by-PT-02 side effect (cycle-7 note carried forward)

Auto-chains to Step 16 (Deploy).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:10:12 +03:00
Oleksandr Bezdieniezhnykh 8fca6e0209 [AZ-809] F-AZ809-1: cap geofences.polygons at 50 (security audit)
Closes the cycle-8 Medium DoS finding. Without the cap, an
authenticated caller could submit millions of bbox polygons in a
single 500 MiB request (Kestrel global limit) and saturate the
FluentValidation allocator on the validator hot path; each polygon
is ~90 bytes of JSON, so the body limit is not a useful gate.

Realistic use is 1-10 polygons per route — 50 leaves 5x headroom
while bounding the worst-case allocation.

Layers:
- CreateRouteRequestValidator: MaxPolygons = 50 + Must(...) chained
  before RuleForEach so the count error fires at "geofences.polygons"
  (not the leaf path).
- Unit: Validate_GeofencePolygonsTooMany_FailsCountRule.
- Integration: GeofencePolygonsTooMany_Returns400 (51 valid bbox
  polygons -> HTTP 400 + errors["geofences.polygons"]).
- Contract: route-creation.md -> v1.0.1 patch (tightening an
  existing range). New Inv-10, new geofence-polygons-too-many
  test case, changelog row.
- Test spec: BT-29 sub-case 9b + AZ-809 AC-1b row in the
  traceability matrix.
- Security report: F-AZ809-1 marked RESOLVED in cycle 8; verdict
  remains PASS_WITH_WARNINGS (Lows + carry-overs unchanged).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:29:10 +03:00
Oleksandr Bezdieniezhnykh ac40a8b352 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 security audit
PASS_WITH_WARNINGS. Zero Critical / High.

New cycle-8 findings:
- F-AZ809-1 (Medium / A04 Insecure Design): unbounded
  geofences.polygons enables an authenticated DoS on
  POST /api/satellite/route. Cap candidate: 50 or 500.
- F-AZ810-1 (Low / A09): JsonException.Message echoed in
  UavUploadValidationFilter (new instance of cycle-7 F-AZ795-1
  pattern in a second code path).
- F-AZ810-2 (Low / Informational): UavTileMetadata.CapturedAt
  typed DateTime not DateTimeOffset; freshness window drifts in
  non-UTC dev environments. Zero impact in UTC-deployed prod.

Carry-overs (cycle 7): F-AZ795-1, F-AZ795-2, D-AZ795-1 still
open. Cycle 4 D2-cy4 still open (test-runtime Medium).

Cycle-8 architectural wins recorded: per-endpoint validation
reached 100% coverage; three approved validation paths
formalised; OSM wire-format normalisation under strict mode
(AZ-812); UAV-handler defence-in-depth retained.

Highest-priority cycle-9 follow-up: F-AZ809-1 polygon cap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:17:31 +03:00
Oleksandr Bezdieniezhnykh 6207ab7c27 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 docs sync
Phase 13 of autodev existing-code flow — document skill in task
mode. Targeted updates to system-level docs that the per-batch
implementation commits did not already cover. Per-module docs
(api_program.md, common_dtos.md, system-flows.md F1/F2/F4) and
the 4 new contract docs (region-request.md, route-creation.md,
tile-latlon.md, uav-tile-upload.md v1.2.0) were already updated
during Step 10 batch commits and were verified-clean here.

architecture.md
- Bump contracts inventory line to mention uav-tile-upload.md v1.2.0
  (was v1.1.0) and add the four cycle-8 contracts (region-request,
  route-creation, tile-latlon, error-shape) so the contract index
  in architecture.md is no longer stale relative to the implemented
  endpoints.
- Add new architectural principle "Strict wire-format validation
  at the API edge (AZ-795 epic, completed across cycles 7-8)" to
  the Architectural Principles list. Describes the two-layer
  enforcement (deserializer + FluentValidation), the three approved
  per-endpoint paths (WithValidation<T> for JSON bodies,
  UavUploadValidationFilter for multipart, RejectUnknownQueryParams
  EndpointFilter + WithValidation<TQuery> for query strings), and
  the no-handler-without-validation rule.

ripple_log_cycle8.md
- New cycle-8 ripple log following the cycle-7 template. Documents
  every directly-changed source file, the importer scan results,
  doc refresh decisions, and the no-ripple component list.
- Records the AZ-795 epic posture: cycle 8 closes the per-endpoint
  rollout. Every public-facing JSON, multipart, and query-param
  endpoint now goes through one of the three approved paths. The
  exempt endpoints (GET region/{id}, GET route/{id}, GET tiles/mgrs
  stub, GET tiles/{z}/{x}/{y}) are listed with justification.

State
- Advance autodev to Step 14 (Security Audit), sub_step phase 0
  awaiting-choice.

No production code change; no test code change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:32:57 +03:00
Oleksandr Bezdieniezhnykh ec0eb909a1 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 test-spec sync
Phase 12 of autodev existing-code flow — cycle-update mode of the
test-spec skill. Append cycle-8 coverage to the documentation suite
without rewriting any pre-cycle-8 content.

blackbox-tests.md
- Add 4 new BT entries (BT-28..BT-31) — one per cycle-8 endpoint:
  - BT-28: Region request endpoint strict validation + OSM rename
    (AZ-808 + AZ-812; 11 sub-cases through the new `RegionRequest
    Validator` + the AZ-795 deserializer infra; sub-case `pos` proves
    the new `lat`/`lon` names accepted, sub-case `9` proves the old
    `latitude`/`longitude` rejected by `UnmappedMemberHandling.Disallow`).
  - BT-29: Create route endpoint nested + cross-field validation
    (AZ-809; 15 sub-cases covering nested per-point validators,
    geofence cross-field invariants, and the `createTilesZip` /
    `requestMaps` cross-field rule; advisory ACs 9 + 10 explicitly
    NOT tested per spec).
  - BT-30: UAV upload metadata multipart validation (AZ-810; 14
    sub-cases across the three-layer composition: deserializer,
    FluentValidation, envelope cross-field; documents the unique
    `errors["metadata"]` vs `errors["metadata.items[i].field"]` key
    convention for multipart endpoints).
  - BT-31: GET tiles/latlon query-param validation + unknown-param
    rejection (AZ-811; 8 sub-cases; sub-cases 4b + 4c prove the
    novel `UnknownQueryParameterEndpointFilter` rejects both
    legacy and hostile unknown query keys).

traceability-matrix.md
- Append 41 AC rows (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).
- Update Coverage Summary: cycle-8 row added; Total moves from
  126 tests / 75 ACs to 167 tests / 116 ACs.
- Add "Coverage shape notes (Cycle 8 ...)" section explaining the
  multipart enforcement shape (AZ-810), the new query-param filter
  (AZ-811), the AZ-808 + AZ-812 same-cycle coordination, and the
  AZ-810 AC-9 process annotation (false-PASS by source tracing →
  bound to green full-suite re-run after the test-data coord-clamp
  fix in commit b763da3).
- AZ-809 AC-9 + AC-10 marked as `◐ advisory (not tested)` —
  naming-consistency concerns surfaced for parent-suite team
  decision.

State
- Advance autodev to Step 13 (Update Docs), sub_step phase 0
  awaiting-invocation.

No production code change; no contract change; no test code change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:28:17 +03:00
19 changed files with 1441 additions and 23 deletions
@@ -27,6 +27,15 @@ public sealed class CreateRouteRequestValidator : AbstractValidator<CreateRouteR
private const int MaxPoints = 500; private const int MaxPoints = 500;
private const int MaxNameLength = 200; private const int MaxNameLength = 200;
private const int MaxDescriptionLength = 1000; private const int MaxDescriptionLength = 1000;
// Geofences are axis-aligned bbox rectangles used for AOI restriction
// during route planning (see route-creation.md). Realistic use is 1-10
// polygons per route; cap at 50 to give 5x headroom while bounding the
// validator's worst-case allocation. The global Kestrel body limit
// (500 MiB, sized for the UAV upload endpoint) is not a useful gate
// here because polygon JSON is small (~90 bytes per minimum-shape
// polygon); without this cap a single authenticated request could
// submit millions of polygons and saturate the LOH.
private const int MaxPolygons = 50;
public CreateRouteRequestValidator() public CreateRouteRequestValidator()
{ {
@@ -74,6 +83,8 @@ public sealed class CreateRouteRequestValidator : AbstractValidator<CreateRouteR
RuleFor(req => req.Geofences!.Polygons) RuleFor(req => req.Geofences!.Polygons)
.NotNull().WithMessage("`geofences.polygons` is required when `geofences` is present.") .NotNull().WithMessage("`geofences.polygons` is required when `geofences` is present.")
.NotEmpty().WithMessage("`geofences.polygons` must contain at least 1 polygon when `geofences` is present.") .NotEmpty().WithMessage("`geofences.polygons` must contain at least 1 polygon when `geofences` is present.")
.Must(polygons => polygons is null || polygons.Count <= MaxPolygons)
.WithMessage($"`geofences.polygons` must contain at most {MaxPolygons} polygons.")
.OverridePropertyName("geofences.polygons"); .OverridePropertyName("geofences.polygons");
RuleForEach(req => req.Geofences!.Polygons) RuleForEach(req => req.Geofences!.Polygons)
@@ -49,6 +49,9 @@ public static class CreateRouteValidationTests
// Rule 9: geofence corners + NW-of-SE invariant // Rule 9: geofence corners + NW-of-SE invariant
await GeofenceNwLatNotGreaterThanSeLat_Returns400(httpClient); await GeofenceNwLatNotGreaterThanSeLat_Returns400(httpClient);
// Rule 9b: geofence polygon-count cap (F-AZ809-1 security-audit fix)
await GeofencePolygonsTooMany_Returns400(httpClient);
// Rule 10/11: requestMaps + createTilesZip required // Rule 10/11: requestMaps + createTilesZip required
await MissingRequestMaps_Returns400(httpClient); await MissingRequestMaps_Returns400(httpClient);
@@ -360,6 +363,48 @@ public static class CreateRouteValidationTests
Console.WriteLine(" ✓ NW.lat <= SE.lat rejected by cross-field invariant"); Console.WriteLine(" ✓ NW.lat <= SE.lat rejected by cross-field invariant");
} }
private static async Task GeofencePolygonsTooMany_Returns400(HttpClient httpClient)
{
Console.WriteLine();
Console.WriteLine("AZ-809 rule 9b (security-audit F-AZ809-1): geofence polygon-count > 50 → HTTP 400");
// Arrange — 51 polygons, each valid bbox. Only the count rule should fire.
var routeId = Guid.NewGuid();
var polygonsJson = string.Join(
",\n ",
Enumerable
.Range(0, 51)
.Select(_ => "{ \"northWest\": { \"lat\": 50.15, \"lon\": 36.05 }, \"southEast\": { \"lat\": 50.05, \"lon\": 36.15 } }"));
var body = $$"""
{
"id": "{{routeId}}",
"name": "too-many-polygons",
"regionSizeMeters": 1000,
"zoomLevel": 18,
"points": [
{ "lat": 50.10, "lon": 36.10 },
{ "lat": 50.11, "lon": 36.11 }
],
"geofences": {
"polygons": [
{{polygonsJson}}
]
},
"requestMaps": false,
"createTilesZip": false
}
""";
// Act
var response = await PostJsonAsync(httpClient, body);
var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 geofence polygons too many");
// Assert
ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 geofence polygons too many", expectedErrorPath: "geofences.polygons");
Console.WriteLine(" ✓ 51 polygons rejected with errors[\"geofences.polygons\"] (cap is 50)");
}
private static async Task MissingRequestMaps_Returns400(HttpClient httpClient) private static async Task MissingRequestMaps_Returns400(HttpClient httpClient)
{ {
Console.WriteLine(); Console.WriteLine();
@@ -249,6 +249,33 @@ public class CreateRouteRequestValidatorTests
result.ShouldHaveValidationErrorFor("geofences.polygons"); result.ShouldHaveValidationErrorFor("geofences.polygons");
} }
[Fact]
public void Validate_GeofencePolygonsTooMany_FailsCountRule()
{
// Arrange — 51 polygons; cap is 50 (security-audit F-AZ809-1 fix).
// Each polygon is a valid bbox so only the count rule should fire.
var request = ValidRequest();
request.Geofences = new Geofences
{
Polygons = Enumerable
.Range(0, 51)
.Select(_ => new GeofencePolygon
{
NorthWest = new GeoPoint(50.15, 36.05),
SouthEast = new GeoPoint(50.05, 36.15),
})
.ToList(),
};
// Act
var result = _validator.TestValidate(request);
// Assert — OverridePropertyName makes the count rule fire at the
// wire-format path `geofences.polygons` (not the leaf-only `polygons`).
result.ShouldHaveValidationErrorFor("geofences.polygons")
.WithErrorMessage("`geofences.polygons` must contain at most 50 polygons.");
}
[Fact] [Fact]
public void Validate_CreateTilesZipWithoutRequestMaps_FailsCrossFieldRule() public void Validate_CreateTilesZipWithoutRequestMaps_FailsCrossFieldRule()
{ {
+2 -1
View File
@@ -24,6 +24,7 @@ The three Layer-3 service components are compile-time siblings: each only refere
- Cross-repo deterministic tile identity (AZ-503) — the `TileNamespace` UUID `5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c` and the canonical name format are shared with the sibling workspace `gps-denied-onboard` (`components/c6_tile_cache/_uuid.py:TILE_NAMESPACE`). Both sides MUST produce byte-identical UUIDv5 output so an onboard-cached tile and a server-cached tile for the same `(z, x, y, source, flight_id)` are recognized as the same artifact without a round-trip. Changing the namespace constant on either side is a coordinated cross-repo break. (`inferred-from: Uuidv5.cs, AZ-503 task spec § Constraints`) - Cross-repo deterministic tile identity (AZ-503) — the `TileNamespace` UUID `5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c` and the canonical name format are shared with the sibling workspace `gps-denied-onboard` (`components/c6_tile_cache/_uuid.py:TILE_NAMESPACE`). Both sides MUST produce byte-identical UUIDv5 output so an onboard-cached tile and a server-cached tile for the same `(z, x, y, source, flight_id)` are recognized as the same artifact without a round-trip. Changing the namespace constant on either side is a coordinated cross-repo break. (`inferred-from: Uuidv5.cs, AZ-503 task spec § Constraints`)
- Fire-and-forget async processing with status polling (`inferred-from: queue + background service + status endpoint`) - Fire-and-forget async processing with status polling (`inferred-from: queue + background service + status endpoint`)
- JWT-validated callers only — every HTTP endpoint requires a valid HS256-signed Bearer token, validated locally against a shared `JWT_SECRET` per the suite-level auth contract (`suite/_docs/10_auth.md`). Issuer/audience are intentionally not validated yet; signature + lifetime + ≥32-byte key are. Per-endpoint permission claims (e.g. `permissions: ["GPS"]` on the UAV upload) layer on top of this baseline. - JWT-validated callers only — every HTTP endpoint requires a valid HS256-signed Bearer token, validated locally against a shared `JWT_SECRET` per the suite-level auth contract (`suite/_docs/10_auth.md`). Issuer/audience are intentionally not validated yet; signature + lifetime + ≥32-byte key are. Per-endpoint permission claims (e.g. `permissions: ["GPS"]` on the UAV upload) layer on top of this baseline.
- Strict wire-format validation at the API edge (AZ-795 epic, completed across cycles 7-8) — every public-facing endpoint runs every incoming payload through two collaborating layers BEFORE the handler sees it: (a) `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + `[JsonRequired]` on every non-optional DTO axis at the System.Text.Json deserializer; (b) per-endpoint FluentValidation `IValidator<T>` wired via `WithValidation<T>()` (JSON bodies) or `UavUploadValidationFilter` (multipart) or `RejectUnknownQueryParamsEndpointFilter` + `GetTileByLatLonQueryValidator` (query params). Both layers produce identically-shaped RFC 7807 `ValidationProblemDetails` per `error-shape.md` v1.0.0, so callers see one error contract regardless of which layer fired. The principle is: **no payload reaches a handler unless every field is present, every type matches, every range is honored, and no unknown field was silently dropped.** This closes the silent-coercion footgun class (e.g. missing `id` → zero-Guid → untracked region/route; typo `?latitude=``lat=0`; misnamed `{"Latitude":...}``lat=0`) that pre-cycle-7 produced misleading 200-OK responses. Adding a new public endpoint requires either a `WithValidation<T>()` chain (JSON), a `UavUploadValidationFilter`-style multipart filter, or an `RejectUnknownQueryParamsEndpointFilter` + query validator (query string) — there is no other approved path.
**Authentication & Authorization** (AZ-487): **Authentication & Authorization** (AZ-487):
- Validation library: `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (matches `Microsoft.AspNetCore.OpenApi` 10.0.7; AZ-496 bumped both packages from 8.0.21 → 8.0.25 in cycle 3 to close the cycle-1 D1 + cycle-2 D3 supply-chain findings, then AZ-500 bumped both 8.0.25 → 10.0.7 in cycle 4 as part of the .NET 8 → .NET 10 migration). The `TokenValidationParameters` shape is unchanged across the JwtBearer 8 → 10 jump — AZ-487/AZ-494 integration tests are the gate and all pass on .NET 10. - Validation library: `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (matches `Microsoft.AspNetCore.OpenApi` 10.0.7; AZ-496 bumped both packages from 8.0.21 → 8.0.25 in cycle 3 to close the cycle-1 D1 + cycle-2 D3 supply-chain findings, then AZ-500 bumped both 8.0.25 → 10.0.7 in cycle 4 as part of the .NET 8 → .NET 10 migration). The `TokenValidationParameters` shape is unchanged across the JwtBearer 8 → 10 jump — AZ-487/AZ-494 integration tests are the gate and all pass on .NET 10.
@@ -39,7 +40,7 @@ The three Layer-3 service components are compile-time siblings: each only refere
- *Google Maps* — `TileService.DownloadAndStoreTilesAsync` / `DownloadAndStoreSingleTileAsync` stamp `source='google_maps'`, `flight_id=NULL`, and a deterministic UUIDv5 `id` on every persisted row; tile JPEGs live under `{StorageConfig.TilesDirectory}/{zoom}/...` per the legacy grandfathered layout. `content_sha256` is computed from the on-disk JPEG body. - *Google Maps* — `TileService.DownloadAndStoreTilesAsync` / `DownloadAndStoreSingleTileAsync` stamp `source='google_maps'`, `flight_id=NULL`, and a deterministic UUIDv5 `id` on every persisted row; tile JPEGs live under `{StorageConfig.TilesDirectory}/{zoom}/...` per the legacy grandfathered layout. `content_sha256` is computed from the on-disk JPEG body.
- *UAV* — `POST /api/satellite/upload` (AZ-488; per-flight key extended by AZ-503) accepts a multipart batch of UAV-captured tiles, runs each item through a 5-rule quality gate (`UavTileQualityGate`), and persists accepted items via `ITileRepository.InsertAsync` with `source='uav'`, `flight_id = metadata.flightId` (or NULL for anonymous uploads), and a deterministic UUIDv5 `id`. UAV JPEGs live under `{StorageConfig.TilesDirectory}/uav/{flight_id or 'none'}/{zoom}/{x}/{y}.jpg`, so `rm -rf ./tiles/uav/{flight_id}/` removes one flight's evidence without touching other flights at overlapping cells. Requires the `GPS` permission claim on top of the JWT baseline. - *UAV* — `POST /api/satellite/upload` (AZ-488; per-flight key extended by AZ-503) accepts a multipart batch of UAV-captured tiles, runs each item through a 5-rule quality gate (`UavTileQualityGate`), and persists accepted items via `ITileRepository.InsertAsync` with `source='uav'`, `flight_id = metadata.flightId` (or NULL for anonymous uploads), and a deterministic UUIDv5 `id`. UAV JPEGs live under `{StorageConfig.TilesDirectory}/uav/{flight_id or 'none'}/{zoom}/{x}/{y}.jpg`, so `rm -rf ./tiles/uav/{flight_id}/` removes one flight's evidence without touching other flights at overlapping cells. Requires the `GPS` permission claim on top of the JWT baseline.
The N-source storage contract is authoritative in `_docs/02_document/contracts/data-access/tile-storage.md` (v2.0.0 — bumped jointly by AZ-503-foundation and AZ-505 in cycle 6 to capture the identity columns, `tiles_leaflet_path` covering index, and `location_hash`-keyed leaflet read rule). The UAV upload contract is authoritative in `_docs/02_document/contracts/api/uav-tile-upload.md` (v1.1.0; AZ-503 added an optional `flightId` field to per-item metadata — backward-compatible). The bulk tile-inventory contract is authoritative in `_docs/02_document/contracts/api/tile-inventory.md` (v1.0.0; AZ-505). Anything that reads or writes `tiles` MUST follow those contracts rather than re-deriving the rules from prose here. The N-source storage contract is authoritative in `_docs/02_document/contracts/data-access/tile-storage.md` (v2.0.0 — bumped jointly by AZ-503-foundation and AZ-505 in cycle 6 to capture the identity columns, `tiles_leaflet_path` covering index, and `location_hash`-keyed leaflet read rule). The UAV upload contract is authoritative in `_docs/02_document/contracts/api/uav-tile-upload.md` (v1.2.0; AZ-503 added an optional `flightId` field to per-item metadata in v1.1.0, AZ-810 cycle 8 added the strict metadata-validation section in v1.2.0). The bulk tile-inventory contract is authoritative in `_docs/02_document/contracts/api/tile-inventory.md` (v2.0.0; AZ-505 v1.0.0, AZ-794+AZ-796 cycle 7 bumped to v2.0.0 with the OSM `z/x/y` rename + strict validation rules). The four wire-format contracts added in cycle 8 are authoritative for their respective endpoints: `_docs/02_document/contracts/api/region-request.md` v1.0.0 (`POST /api/satellite/request`, AZ-808+AZ-812), `_docs/02_document/contracts/api/route-creation.md` v1.0.0 (`POST /api/satellite/route`, AZ-809), `_docs/02_document/contracts/api/tile-latlon.md` v1.0.0 (`GET /api/satellite/tiles/latlon`, AZ-811), and `_docs/02_document/contracts/api/error-shape.md` v1.0.0 (the cross-endpoint RFC 7807 `ValidationProblemDetails` envelope shared by every validating endpoint, AZ-795 cycle 7). Anything that reads or writes `tiles` MUST follow those contracts rather than re-deriving the rules from prose here.
**Drift signals**: **Drift signals**:
- `geofence_polygons` mentioned in AGENTS.md as a routes table column but does not exist in schema or entity — documentation drift - `geofence_polygons` mentioned in AGENTS.md as a routes table column but does not exist in schema or entity — documentation drift
@@ -3,9 +3,9 @@
**Component**: WebApi (`SatelliteProvider.Api`) producing rows via RouteManagement (`SatelliteProvider.Services.RouteManagement`) and feeding the background Route Map Processing flow (Flow F5) **Component**: WebApi (`SatelliteProvider.Api`) producing rows via RouteManagement (`SatelliteProvider.Services.RouteManagement`) and feeding the background Route Map Processing flow (Flow F5)
**Producer task**: AZ-809 — `_docs/02_tasks/done/AZ-809_route_endpoint_validation.md` (validator + this contract) **Producer task**: AZ-809 — `_docs/02_tasks/done/AZ-809_route_endpoint_validation.md` (validator + this contract)
**Consumer tasks**: `gps-denied-onboard` AZ-777 Phase 2 (preferred imagery-seeding path — route-based rather than bbox-based) **Consumer tasks**: `gps-denied-onboard` AZ-777 Phase 2 (preferred imagery-seeding path — route-based rather than bbox-based)
**Version**: 1.0.0 **Version**: 1.0.1
**Status**: frozen **Status**: frozen
**Last Updated**: 2026-05-22 **Last Updated**: 2026-05-23
## Purpose ## Purpose
@@ -62,7 +62,7 @@ Per-field constraints:
| `points[i].lat` | number | yes (`[JsonRequired]`) | WGS84 latitude. | `[-90.0, 90.0]`. | | `points[i].lat` | number | yes (`[JsonRequired]`) | WGS84 latitude. | `[-90.0, 90.0]`. |
| `points[i].lon` | number | yes (`[JsonRequired]`) | WGS84 longitude. | `[-180.0, 180.0]`. | | `points[i].lon` | number | yes (`[JsonRequired]`) | WGS84 longitude. | `[-180.0, 180.0]`. |
| `geofences` | object | no | When present, intermediate points outside ALL polygons get filtered before region enqueue. | See nested shape below. | | `geofences` | object | no | When present, intermediate points outside ALL polygons get filtered before region enqueue. | See nested shape below. |
| `geofences.polygons` | array | yes (`[JsonRequired]` when `geofences` present) | One or more bbox polygons (NW corner + SE corner). | Non-empty when `geofences` present. | | `geofences.polygons` | array | yes (`[JsonRequired]` when `geofences` present) | One or more bbox polygons (NW corner + SE corner). | Count `[1, 50]` when `geofences` present. |
| `geofences.polygons[i].northWest` | object | yes (`[JsonRequired]`) | Polygon's northwest corner. | See `GeoPoint` shape. | | `geofences.polygons[i].northWest` | object | yes (`[JsonRequired]`) | Polygon's northwest corner. | See `GeoPoint` shape. |
| `geofences.polygons[i].southEast` | object | yes (`[JsonRequired]`) | Polygon's southeast corner. | See `GeoPoint` shape. | | `geofences.polygons[i].southEast` | object | yes (`[JsonRequired]`) | Polygon's southeast corner. | See `GeoPoint` shape. |
| `requestMaps` | bool | yes (`[JsonRequired]`) | When `true`, enqueue background region-requests for every route point inside the geofences (or all points if no geofences). | No default — caller must declare intent. | | `requestMaps` | bool | yes (`[JsonRequired]`) | When `true`, enqueue background region-requests for every route point inside the geofences (or all points if no geofences). | No default — caller must declare intent. |
@@ -166,6 +166,7 @@ Example body for a polygon corner invariant failure:
- **Inv-7** (per-polygon shape): `northWest` AND `southEast` corners both present. - **Inv-7** (per-polygon shape): `northWest` AND `southEast` corners both present.
- **Inv-8** (per-polygon invariant): `northWest.lat > southEast.lat` AND `northWest.lon < southEast.lon`. - **Inv-8** (per-polygon invariant): `northWest.lat > southEast.lat` AND `northWest.lon < southEast.lon`.
- **Inv-9**: Unknown root or nested fields → 400 (deserializer's `UnmappedMemberHandling.Disallow`). - **Inv-9**: Unknown root or nested fields → 400 (deserializer's `UnmappedMemberHandling.Disallow`).
- **Inv-10**: When `geofences` is present, `geofences.polygons.Count` is in `[1, 50]`. Cap to bound validator allocation worst case — see `_docs/05_security/security_report_cycle8.md` § F-AZ809-1. Realistic use is 1-10 polygons per route; the 50 cap leaves 5x headroom.
## Non-Goals ## Non-Goals
@@ -199,6 +200,7 @@ Example body for a polygon corner invariant failure:
| point-lon-out-of-range | `"points":[..., {"lat":..,"lon":181}]` | HTTP 400 + `errors["points[1].lon"]` | Rule 8 | | point-lon-out-of-range | `"points":[..., {"lat":..,"lon":181}]` | HTTP 400 + `errors["points[1].lon"]` | Rule 8 |
| geofence-nw-not-north | NW.lat == SE.lat | HTTP 400 + `errors["geofences.polygons[0].northWest"]` | Rule 9 / Inv-8 | | geofence-nw-not-north | NW.lat == SE.lat | HTTP 400 + `errors["geofences.polygons[0].northWest"]` | Rule 9 / Inv-8 |
| geofence-nw-not-west | NW.lon == SE.lon | HTTP 400 + `errors["geofences.polygons[0].northWest"]` | Rule 9 / Inv-8 | | geofence-nw-not-west | NW.lon == SE.lon | HTTP 400 + `errors["geofences.polygons[0].northWest"]` | Rule 9 / Inv-8 |
| geofence-polygons-too-many | 51-polygon array | HTTP 400 + `errors["geofences.polygons"]` ("must contain at most 50 polygons.") | Rule 9b / Inv-10 (F-AZ809-1 follow-up) |
| missing-requestMaps | body without `requestMaps` | HTTP 400 + `errors[requestMaps]` | Rule 10 | | missing-requestMaps | body without `requestMaps` | HTTP 400 + `errors[requestMaps]` | Rule 10 |
| createTilesZip-without-requestMaps | `"requestMaps":false,"createTilesZip":true` | HTTP 400 + `errors[createTilesZip]` | Rule 12 (cross-field) | | createTilesZip-without-requestMaps | `"requestMaps":false,"createTilesZip":true` | HTTP 400 + `errors[createTilesZip]` | Rule 12 (cross-field) |
| unknown-root-field | extra `"debug":"..."` key | HTTP 400 + `errors[debug]` | Rule 13 (`UnmappedMemberHandling.Disallow`) | | unknown-root-field | extra `"debug":"..."` key | HTTP 400 + `errors[debug]` | Rule 13 (`UnmappedMemberHandling.Disallow`) |
@@ -211,3 +213,4 @@ Example body for a polygon corner invariant failure:
| Version | Date | Change | Author | | Version | Date | Change | Author |
|---------|------|--------|--------| |---------|------|--------|--------|
| 1.0.0 | 2026-05-22 | Initial contract for `POST /api/satellite/route`. Publishes the FluentValidation surface (CreateRouteRequestValidator + RoutePointValidator + GeofencePolygonValidator) + the 14 rules in AZ-809, including the probe-confirmed missing-id gap and the cross-field `createTilesZip ⇒ requestMaps` invariant. References `error-shape.md` v1.0.0, `region-request.md` v1.0.0 (F5 enqueue path), and Flows F4/F5 (cross-link). | autodev (Step 10, cycle 8) | | 1.0.0 | 2026-05-22 | Initial contract for `POST /api/satellite/route`. Publishes the FluentValidation surface (CreateRouteRequestValidator + RoutePointValidator + GeofencePolygonValidator) + the 14 rules in AZ-809, including the probe-confirmed missing-id gap and the cross-field `createTilesZip ⇒ requestMaps` invariant. References `error-shape.md` v1.0.0, `region-request.md` v1.0.0 (F5 enqueue path), and Flows F4/F5 (cross-link). | autodev (Step 10, cycle 8) |
| 1.0.1 | 2026-05-23 | Tighten `geofences.polygons` constraint from "non-empty" to "Count `[1, 50]`". New Inv-10 + test case `geofence-polygons-too-many`. Patch release per Versioning Rules (tightening an existing range). The 50-polygon cap closes a defence-gap surfaced by the cycle-8 security audit: without the cap, an authenticated caller could submit millions of polygons in a single 500 MiB request and saturate the validator's allocation heap. Realistic use is 1-10 polygons per route. | autodev (Step 14 follow-up, cycle 8) |
+105
View File
@@ -0,0 +1,105 @@
# 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<RequestRegionRequest>()` on `MapPost("/api/satellite/request", …)` (AZ-808).
- `.WithValidation<CreateRouteRequest>()` on `MapPost("/api/satellite/route", …)` (AZ-809).
- `.AddEndpointFilter<UavUploadValidationFilter>()` on `MapPost("/api/satellite/upload", …)` (AZ-810) — bespoke multipart filter, not the generic `WithValidation<T>()`.
- `.AddEndpointFilter(new RejectUnknownQueryParamsEndpointFilter("lat", "lon", "zoom"))` + `.WithValidation<GetTileByLatLonQuery>()` on `MapGet("/api/satellite/tiles/latlon", …)` (AZ-811) — two-filter chain so unknown-key rejection precedes range checks.
- `AddTransient<UavUploadValidationFilter>()` 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<RequestRegionRequest>` 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<UavTileBatchMetadataPayload>` 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<GetTileByLatLonQuery>` with `lat`/`lon`/`zoom` rules using `Cascade(CascadeMode.Stop) → NotNull → InclusiveBetween` so missing-param surfaces only as `"\`<paramName>\` 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<T>()` + `ValidationEndpointFilter<T>` + `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<TQuery>()`. 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.
+109
View File
@@ -284,3 +284,112 @@ The cycle introduces no new HTTP routes. Functional positive coverage is unchang
**AC trace**: AZ-796 AC-1 (all 9 rules + ProblemDetails shape), AC-2 (happy path); AZ-794 AC-1 (positive z/x/y acceptance — sub-case `pos`), AZ-794 AC-2 (sub-case `9c` proves the old names produce a structured 400, eliminating the silent-coerce-to-0 footgun); AZ-795 (epic-level — every sub-case exercises the shared `ValidationEndpointFilter` + `GlobalExceptionHandler` + `UnmappedMemberHandling.Disallow` infra). **AC trace**: AZ-796 AC-1 (all 9 rules + ProblemDetails shape), AC-2 (happy path); AZ-794 AC-1 (positive z/x/y acceptance — sub-case `pos`), AZ-794 AC-2 (sub-case `9c` proves the old names produce a structured 400, eliminating the silent-coerce-to-0 footgun); AZ-795 (epic-level — every sub-case exercises the shared `ValidationEndpointFilter` + `GlobalExceptionHandler` + `UnmappedMemberHandling.Disallow` infra).
**Notes**: The 9 rules split across two enforcement layers — rules 5/6/9 are enforced by the deserializer (`JsonRequired` + `UnmappedMemberHandling.Disallow` + native JSON type validation, see AZ-795 shared infra) and surface as `BadHttpRequestException``GlobalExceptionHandler.JsonException` branch; rules 2/3/4/7/8 are enforced by `InventoryRequestValidator` (FluentValidation) via `ValidationEndpointFilter<TileInventoryRequest>`. Both paths produce identically-shaped `ValidationProblemDetails` bodies (`error-shape.md` v1.0.0 invariant). **Notes**: The 9 rules split across two enforcement layers — rules 5/6/9 are enforced by the deserializer (`JsonRequired` + `UnmappedMemberHandling.Disallow` + native JSON type validation, see AZ-795 shared infra) and surface as `BadHttpRequestException``GlobalExceptionHandler.JsonException` branch; rules 2/3/4/7/8 are enforced by `InventoryRequestValidator` (FluentValidation) via `ValidationEndpointFilter<TileInventoryRequest>`. Both paths produce identically-shaped `ValidationProblemDetails` bodies (`error-shape.md` v1.0.0 invariant).
---
## Cycle 8 — AZ-808 / AZ-809 / AZ-810 / AZ-811 / AZ-812 Strict Validation Sweep + Region Rename
Cycle 8 extends the AZ-795 shared validation infrastructure (FluentValidation + `ValidationEndpointFilter<T>` + `GlobalExceptionHandler` + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`) to the four remaining public-facing endpoints, and lands the OSM-convention `Lat`/`Lon` rename on the Region API in the same commit set. Every cycle-8 endpoint emits the same `ValidationProblemDetails` shape (`error-shape.md` v1.0.0) used by AZ-796. No new HTTP routes; existing positive coverage (BT-01..BT-18 region/route/tile flows, BT-13..BT-17 UAV upload, BT-N01..BT-N05 negatives) is preserved. The new tests below cover the strict-rejection behaviour that pre-cycle-8 either silently coerced (missing `id` → zero-Guid; unknown `?latitude=``lat=0`) or accepted out-of-range (lat > 90, zoom > 22) values that downstream code couldn't render.
## BT-28: Region Request Endpoint — Strict Validation + OSM Rename
**Trigger**: A family of `POST /api/satellite/request` calls. AZ-812's `Lat`/`Lon` rename ships in the same commit as AZ-808; every sub-case uses the post-rename wire shape `{"id":"<guid>","lat":..,"lon":..,"sizeMeters":..,"zoomLevel":..,"stitchTiles":..}`.
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `region-request.md` v1.0.0 frozen (v1.0.0 published directly with the post-AZ-812 names per AZ-812 AC-6 coordination).
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `RegionStatusResponse` for the happy path. `errors[]` names the offending field path in request-body camelCase.
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|---|------|-----------------|-----------------------|-------------|
| pos | Happy path with `lat`/`lon` | full valid body | HTTP 200 — body shape unchanged from pre-AZ-808 (`{regionId,status}`) | `HappyPath_Returns200` |
| 1 | Empty body | zero bytes, `Content-Type: application/json` | (framework-level ProblemDetails) | `EmptyBody_Returns400` |
| 2a | Required `id` (silent-coercion fix) | omit `id` entirely | `id` — message states `id` is required (NOT zero-Guid coercion) | `Post_WithMissingId_ReturnsBadRequest` |
| 2b | Zero-Guid `id` | `"id":"00000000-0000-0000-0000-000000000000"` | `id` | `ZeroGuidId_Returns400` |
| 3 | `lat` in range `[-90, 90]` | `"lat":91` | `lat` | `LatOutOfRange_Returns400` |
| 4 | `lon` in range `[-180, 180]` | `"lon":181` | `lon` | `LonOutOfRange_Returns400` |
| 5 | `sizeMeters` in range `[100, 10_000]` | `"sizeMeters":1000000` | `sizeMeters` | `SizeMetersOutOfRange_Returns400` |
| 6 | `zoomLevel` in range `[0, 22]` | `"zoomLevel":30` | `zoomLevel` | `ZoomOutOfRange_Returns400` |
| 7 | Required `stitchTiles` | omit `stitchTiles` | `stitchTiles` | `MissingStitchTiles_Returns400` |
| 8 | Unknown root field (`UnmappedMemberHandling.Disallow`) | `"debug":42` | path mentioning `debug` | `UnknownRootField_Returns400` |
| 9 | Legacy v1 names (`latitude`/`longitude`) | exact AZ-777 Phase 2 reproduction body | path mentioning `latitude` (treated as unknown post-AZ-812) | `OldLatLongNames_Returns400` |
| 10 | Type mismatch on `lat` | `"lat":"fifty"` | path mentioning `lat` | `LatTypeMismatch_Returns400` |
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key OR an equivalent RFC 7807 body for the empty-body case; the happy path returns HTTP 200 + a non-empty `regionId`. AZ-812 AC-4 is verified by sub-cases `pos` (new names accepted) and `9` (old names rejected by `UnmappedMemberHandling.Disallow`) on the same endpoint. No regression in `RegionRequestTests.cs` (cycle 8 Step 11 — green).
**AC trace**: AZ-808 AC-1 (8 rules + ProblemDetails shape), AC-2 (happy path); AZ-812 AC-2 (wire format end-to-end), AC-3 (existing happy-path tests pass — Step 11 green), AC-4 (post-rename accepted, pre-rename rejected — sub-cases `pos` + `9`).
**Notes**: The 8 validations split between the deserializer (rule 7 missing-required, rule 8 unknown-root, rule 10 type-mismatch — surface via `GlobalExceptionHandler`) and `RegionRequestValidator` (rules 2b/3/4/5/6 — surface via `ValidationEndpointFilter<RequestRegionRequest>`). The silent-coercion case (rule 2a) is the AZ-777 Phase 2 reproducer: pre-cycle-8 a missing `id` deserialized to `Guid.Empty` and the handler silently created a region with id `00000000-...`; post-cycle-8 it fails-fast at the deserializer.
## BT-29: Create Route Endpoint — Nested + Cross-Field Strict Validation
**Trigger**: A family of `POST /api/satellite/route` calls covering AZ-809's 14 rules. Bodies use the post-AZ-809 wire shape `{id, name, description?, regionSizeMeters, zoomLevel, points: [{lat, lon}, …], requestMaps, createTilesZip, geofences?: {polygons: [{northWest, southEast}]}}`.
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `route-creation.md` v1.0.1 frozen.
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `RouteResponse` for the happy path. `errors[]` keys use the nested path (e.g. `points[1].lat`, `geofences.polygons[0].northWest`).
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|---|------|-----------------|-----------------------|-------------|
| pos | Happy path (`requestMaps=false`, no background processing) | minimal 2-point valid body | HTTP 200 — body shape unchanged | `HappyPath_Returns200` |
| 1 | Empty body | zero bytes | (framework-level ProblemDetails) | `EmptyBody_Returns400` |
| 2a | Required `id` (silent-coercion fix) | omit `id` | `id` is required (no zero-Guid coercion) | `MissingId_Returns400` |
| 2b | Zero-Guid `id` | `"id":"00000000-..."` | `id` | `ZeroGuidId_Returns400` |
| 3 | Required `name` non-empty | `"name":""` | `name` | `EmptyName_Returns400` |
| 5 | `regionSizeMeters` in `[100, 10_000]` | `1000000` | `regionSizeMeters` | `RegionSizeMetersOutOfRange_Returns400` |
| 6 | `zoomLevel` in `[0, 22]` | `30` | `zoomLevel` | `ZoomOutOfRange_Returns400` |
| 7 | `points` count in `[2, 500]` | 1-point array | `points` | `PointsCountBelowMin_Returns400` |
| 8a | Per-point `lat` in `[-90, 90]` | `points[1].lat=91` | `points[1].lat` | `PointLatOutOfRange_Returns400` |
| 8b | Per-point `lon` in `[-180, 180]` | `points[1].lon=181` | `points[1].lon` | `PointLonOutOfRange_Returns400` |
| 9 | Geofence cross-field invariant (`NW.lat > SE.lat` AND `NW.lon < SE.lon`) | NW.lat ≤ SE.lat | `geofences.polygons[0].northWest` | `GeofenceNwSeInvariantViolated_Returns400` |
| 9b | `geofences.polygons.Count <= 50` (security-audit F-AZ809-1 fix, `route-creation.md` v1.0.1 Inv-10) | 51-polygon array | `geofences.polygons` ("must contain at most 50 polygons.") | `GeofencePolygonsTooMany_Returns400` |
| 10 | Required `requestMaps` (no defaulting) | omit `requestMaps` | `requestMaps` | `MissingRequestMaps_Returns400` |
| 12 | Cross-field: `createTilesZip=true` requires `requestMaps=true` | `{createTilesZip:true, requestMaps:false}` | top-level message (no per-field key) | `CreateTilesZipRequiresRequestMaps_Returns400` |
| 13 | Unknown root field | `"debug":42` | path mentioning `debug` | `UnknownRootField_Returns400` |
| 14 | Nested type mismatch | `"points[0].lat":"fifty"` | path mentioning `points[0].lat` | `NestedTypeMismatch_Returns400` |
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key OR an equivalent RFC 7807 body for the empty/cross-field cases; the happy path returns HTTP 200 + a non-empty `routeId`. No regression in `RouteCreationTests.cs` (cycle 8 Step 11 — green). The AZ-779-class footgun (silent coercion of missing `id` to zero-Guid creating untracked routes) is closed by sub-case 2a.
**AC trace**: AZ-809 AC-1 (14 rules + ProblemDetails shape), AC-2 (happy path). Advisory ACs AC-9 (`sizeMeters` vs `regionSizeMeters` naming) and AC-10 (input `lat`/`lon` vs output `latitude`/`longitude` asymmetry) are explicitly **not** tested — surfaced to parent-suite team for follow-up only.
**Notes**: The 14 rules split across three layers — deserializer (rules 3 type-mismatch, 13 unknown-root, 14 nested type mismatch, 10 missing-required), `CreateRouteRequestValidator` (rules 2b/5/6/7, plus orchestration of nested validators), `RoutePointValidator` (rules 8a/8b — applied per-element via `RuleForEach(x => x.Points).SetValidator`), `GeofencePolygonValidator` (rule 9 cross-field), and a top-level `Must()` (rule 12). Rule 4 (`description` length cap if present) is enforced by `CreateRouteRequestValidator.RuleFor(x => x.Description).MaximumLength(...)` and is not exercised by an explicit sub-case because the existing `pos` body omits `description` and there is no probe-confirmed footgun there. The cross-field rule 12 surfaces under a top-level `errors` map entry (no field key) per FluentValidation convention for `RuleFor(x => x).Must(...)`.
## BT-30: UAV Upload Metadata Endpoint — Multipart Strict Validation
**Trigger**: A family of `POST /api/satellite/upload` multipart calls covering AZ-810's 14 rules. The endpoint is `multipart/form-data`, so the validator wires through a **custom** `UavUploadValidationFilter` (NOT the generic `ValidationEndpointFilter<T>` used by JSON-body endpoints) which deserializes the `metadata` form field via the strict global `JsonSerializerOptions` and routes errors through three composed layers (deserializer → FluentValidation → envelope cross-field check).
**Precondition**: API up; valid JWT with `permissions:["GPS"]` attached. `error-shape.md` v1.0.0 + `uav-tile-upload.md` v1.2.0 frozen.
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + per-item result list for the happy path (per-item file rejections still return HTTP 200 — file-byte semantics unchanged from AZ-488). `errors[]` keys are prefixed with `metadata.` (e.g. `metadata.items[0].latitude`) for FluentValidation-layer rules; deserializer-layer failures surface under `errors["metadata"]` (per `UavUploadValidationFilter` design — the deserializer doesn't know the per-field path inside the form value).
| # | Rule | Layer | Trigger excerpt | Expected `errors` key | Test method |
|---|------|-------|-----------------|-----------------------|-------------|
| pos | Happy path | — | well-formed multipart envelope with valid 256×256 JPEG | HTTP 200, per-item `status=accepted` | `HappyPath_Returns200` |
| 1 | `metadata` form field present | deserializer | omit `metadata` field | `metadata` | `MissingMetadataField_Returns400` |
| 2 | `metadata` deserializes to `UavTileBatchMetadataPayload` | deserializer | `"items":"notanarray"` | `metadata` | `MetadataNotAnObject_Returns400` |
| 3 | `metadata.items` count in `[1, 100]` | FluentValidation | 101-item array | `metadata.items` | `OversizedItemsArray_Returns400` |
| 4 | `metadata.items` count == file count (envelope cross-field) | envelope filter | 2 items, 1 file | both `metadata.items` and `files` | `ItemsFileCountMismatch_Returns400` |
| 5 | per-item `latitude` in `[-90, 90]` | FluentValidation | `items[1].latitude=91` | `metadata.items[1].latitude` | `LatitudeOutOfRange_Returns400` |
| 6 | per-item `longitude` in `[-180, 180]` | FluentValidation | `items[0].longitude=181` | `metadata.items[0].longitude` | `LongitudeOutOfRange_Returns400` |
| 7 | per-item `tileZoom` in `[0, 22]` | FluentValidation | `items[0].tileZoom=30` | `metadata.items[0].tileZoom` | `TileZoomOutOfRange_Returns400` |
| 8 | per-item `tileSizeMeters` > 0 | FluentValidation | `items[0].tileSizeMeters=0` | `metadata.items[0].tileSizeMeters` | `TileSizeMetersZero_Returns400` |
| 9a | per-item `capturedAt` not in future (skew = 30s) | FluentValidation | `capturedAt=UtcNow+1h` | `metadata.items[0].capturedAt` | `CapturedAtFuture_Returns400` |
| 9b | per-item `capturedAt` not older than `MaxAgeDays` (default 7d) | FluentValidation | `capturedAt=UtcNow-60d` | `metadata.items[0].capturedAt` | `CapturedAtTooOld_Returns400` |
| 10 | per-item required fields (`latitude`/`longitude`/`tileZoom`/`tileSizeMeters`/`capturedAt`) present (`[JsonRequired]`) | deserializer | omit `latitude` from one item | `metadata` (deserializer-level; per-field path not available inside form value) | `MissingRequiredField_Returns400` |
| 11 | Unknown root field on `UavTileBatchMetadataPayload` | deserializer | `"debug":42` at metadata root | `metadata` | `UnknownRootField_Returns400` |
| 12 | Unknown nested field on `UavTileMetadata` | deserializer | `"altitude":100` on an item | `metadata` | `UnknownItemField_Returns400` |
| 13 | Type mismatch on per-item field | deserializer | `"latitude":"fifty"` | `metadata` | `LatitudeTypeMismatch_Returns400` |
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key. The happy path returns HTTP 200 + a per-item result list with `status=accepted`. Per-item file rejections (existing `IUavTileQualityGate` semantics: `INVALID_FORMAT`, `WRONG_DIMENSIONS`, `SIZE_OUT_OF_BAND`, `CAPTURED_AT_FUTURE`, `CAPTURED_AT_TOO_OLD`, `IMAGE_TOO_UNIFORM`) still return HTTP 200 with per-item `status=rejected` (AZ-488 contract preserved). **AZ-810 AC-9 verified** by full integration suite green at cycle 8 Step 11 (after the test-data clamp fix — see `_docs/03_implementation/batch_04_cycle8_report.md` AC-9 row + commit `b763da3`).
**AC trace**: AZ-810 AC-1 (14 rules + ProblemDetails shape), AC-2 (happy path unchanged), AC-9 (no AZ-488 regression).
**Notes**: This is the first endpoint where the validation infra had to step **outside** the generic `ValidationEndpointFilter<T>` — multipart form-data is not a JSON body, so the metadata field has to be extracted from `IFormCollection` and deserialized inside a custom filter (`UavUploadValidationFilter`). Three layers compose: (1) deserializer with `JsonSerializerOptions.UnmappedMemberHandling.Disallow` + `[JsonRequired]` — surfaces under `errors["metadata"]`; (2) `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator` (FluentValidation) — error paths prefixed with `metadata.`; (3) envelope cross-field check (`items.Count == files.Count`) inside the filter — surfaces under BOTH `errors["metadata.items"]` AND `errors["files"]`. The `metadata.` prefix and the bare-`"metadata"` key for deserializer-level errors are documented in `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 §Validation Rules.
## BT-31: GET tiles/latlon — Query-Param Strict Validation + Unknown-Param Rejection
**Trigger**: A family of `GET /api/satellite/tiles/latlon?lat=&lon=&zoom=` calls covering AZ-811's 5 rules. The endpoint takes **query parameters** (not a JSON body), so the validator wires through a **novel** `UnknownQueryParameterEndpointFilter` (envelope filter, item 4 of AZ-811's implementation pattern) plus `GetTileByLatLonQueryValidator` (FluentValidation for the typed query params).
**Precondition**: API up; valid JWT attached. `error-shape.md` v1.0.0 + `tile-latlon.md` v1.0.0 frozen.
**Expected**: HTTP 400 with `Content-Type: application/problem+json` for every failure sub-case; HTTP 200 + `DownloadTileResponse` for the happy path. `errors[]` keys name the offending query parameter (e.g. `lat`).
| # | Rule | Trigger excerpt | Expected `errors` key | Test method |
|---|------|-----------------|-----------------------|-------------|
| pos | Happy path | `?lat=47.46&lon=37.64&zoom=18` | HTTP 200 — body shape unchanged | `HappyPath_Returns200` |
| 1 | `lat` in range `[-90, 90]` | `?lat=91` | `lat` | `LatOutOfRange_Returns400` |
| 2 | `lon` in range `[-180, 180]` | `?lon=181` | `lon` | `LonOutOfRange_Returns400` |
| 3 | `zoom` in range `[0, 22]` | `?zoom=30` | `zoom` | `ZoomOutOfRange_Returns400` |
| 4a | `lat` required | omit `?lat=` | `lat` ("`lat` is required") | `MissingLat_Returns400` |
| 4b | Legacy v1 names (`?Latitude=&Longitude=&ZoomLevel=`) | exact pre-AZ-811 wire format | `errors` map names BOTH unknown keys | `LegacyLatitudeLongitudeNames_Returns400` |
| 4c | Hostile / typo query keys | `?debug=1&admin=true` | `errors` map names BOTH unknown keys | `UnknownQueryKeys_Returns400` |
| 5 | `lat` type mismatch | `?lat=fifty` | `lat` | `LatTypeMismatch_Returns400` |
**Pass criterion**: Every failure sub-case returns HTTP 400 with the expected `errors` key. The happy path returns HTTP 200 + a non-empty `tileId` and a path of shape `tiles/{zoom}/{x}/{y}.jpg`. No regression in `TileByLatLonTests.cs` (cycle 8 Step 11 — green). BT-N01 (`lat=91&lon=181&zoom=18`) and BT-N02 (`zoom=25`) — pre-cycle-8 negative scenarios that only asserted "HTTP 4xx or error in body" — are now strictly bound: BT-N01 produces HTTP 400 with `errors["lat"]` AND `errors["lon"]`; BT-N02 produces HTTP 400 with `errors["zoom"]`.
**AC trace**: AZ-811 AC-1 (5 rules + ProblemDetails shape), AC-2 (happy path), AC-9 (the novel unknown-query-param envelope filter is documented in `_docs/02_document/modules/api_program.md` for reuse).
**Notes**: This is the first endpoint to need a generic **unknown-query-param rejection** layer — ASP.NET's default model binder silently ignores unknown query parameters (parallel to `UnmappedMemberHandling.Disallow` for JSON bodies, but no built-in equivalent exists for query strings). The new `UnknownQueryParameterEndpointFilter` introspects the route's declared parameters and rejects any extra keys. Sub-cases `4b` and `4c` exercise this filter: `4b` proves the pre-AZ-811 wire format (`?Latitude=&Longitude=&ZoomLevel=`) that silently fell back to `lat=0, lon=0, zoom=0` now fails fast with HTTP 400 naming all three unknown keys; `4c` proves the same path catches arbitrary hostile / typo keys. The filter is designed for reuse by any future query-param endpoint (AZ-811 AC-9).
+55 -1
View File
@@ -121,6 +121,49 @@
| 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-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-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-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 ## Restrictions → Test Mapping
@@ -171,7 +214,8 @@
| 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 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 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 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%)** | | 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. | — |
| **Total** | **167** | **116/116 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 10 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):** **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 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.
@@ -209,3 +253,13 @@
- 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. - 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. - 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. - 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.
@@ -0,0 +1,60 @@
# Dependency Scan (Cycle 8)
**Date**: 2026-05-23
**Mode**: Delta scan
**Scope**: Cycle-8 delta over the cycle-7 dependency scan (`_docs/05_security/dependency_scan_cycle7.md`). Cycle-8 surface = AZ-808 + AZ-809 + AZ-810 + AZ-811 (strict input validation rolled out across the remaining 4 endpoints) + AZ-812 (region-API wire rename `Latitude`/`Longitude``Lat`/`Lon`).
**Method**: Manifest diff via `git diff --name-only 865dfdb..b763da3 -- '*.csproj'` (cycle-7 tip → cycle-8 tip; verified empty). `dotnet list package --vulnerable` is intentionally not run (per `AGENTS.md`: that command hangs the agent shell in this workspace). The manifest-diff substitute is deterministic because the result is null.
## Cycle-8 Package Manifest Diff
| csproj | Cycle 7 baseline | Cycle 8 change | Net effect on supply chain |
|--------|------------------|----------------|----------------------------|
| `SatelliteProvider.Api/SatelliteProvider.Api.csproj` | references `Microsoft.AspNetCore.OpenApi 10.0.7`, `Microsoft.AspNetCore.Authentication.JwtBearer 10.0.7`, `Newtonsoft.Json 13.0.4`, `Serilog.AspNetCore 8.0.3`, `Serilog.Sinks.File 6.0.0`, `SixLabors.ImageSharp 3.1.11`, `Swashbuckle.AspNetCore 10.1.7`, `FluentValidation 12.0.0`, `FluentValidation.DependencyInjectionExtensions 12.0.0` | **+0 PackageReferences**. Every cycle-8 validator + filter reuses the AZ-795 infrastructure already shipped in cycle 7. | None. |
| `SatelliteProvider.Common/SatelliteProvider.Common.csproj` | unchanged from cycle 5 | **+0 PackageReferences** — the cycle-8 DTO changes (`[JsonRequired]` on `RequestRegionRequest`, `CreateRouteRequest`, `RoutePoint`, `GeoPoint`, `GeofencePolygon.NorthWest/SouthEast`, `Geofences.Polygons`, `UavTileMetadata.*`) and the AZ-812 rename are BCL + `System.Text.Json.Serialization` only. | None. |
| `SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj` | unchanged from cycle 5 | **+0 PackageReferences**. | None. |
| `SatelliteProvider.Services.TileDownloader/SatelliteProvider.Services.TileDownloader.csproj` | unchanged from cycle 5 | **+0 PackageReferences**. The cycle-8 validator path lives in the API project; the existing `UavTileUploadHandler` defence-in-depth path is untouched. | None. |
| `SatelliteProvider.Services.RegionProcessing/SatelliteProvider.Services.RegionProcessing.csproj` | unchanged from cycle 5 | **+0 PackageReferences**. | None. |
| `SatelliteProvider.Services.RouteManagement/SatelliteProvider.Services.RouteManagement.csproj` | unchanged from cycle 5 | **+0 PackageReferences**. | None. |
| `SatelliteProvider.Tests/SatelliteProvider.Tests.csproj` | unchanged from cycle 5 | **+0 PackageReferences** — the 8 new validator unit-test files reuse the cycle-7 `FluentValidation.TestHelper` namespace (transitive via `FluentValidation` main package, picked up via `ProjectReference` to the API). | None. |
| `SatelliteProvider.IntegrationTests/SatelliteProvider.IntegrationTests.csproj` | unchanged from cycle 5 | **+0 PackageReferences** — the 4 new integration test files (`CreateRouteValidationTests`, `GetTileByLatLonValidationTests`, `RegionFieldRenameTests`, `RegionRequestValidationTests`, `UavUploadValidationTests`) reuse the cycle-7 `ProblemDetailsAssertions` helper + the pre-existing `Xunit` + `Microsoft.AspNetCore` ProjectReference. | None. |
| `SatelliteProvider.TestSupport/SatelliteProvider.TestSupport.csproj` | unchanged from cycle 5 | **+0 PackageReferences**. | None. |
**Net cycle-8 dependency change**: **zero new `PackageReference` lines, zero removed lines, zero version bumps**. Every `*.csproj` file in the repo is byte-identical between `865dfdb` (cycle-7 tip) and `b763da3` (cycle-8 tip).
## Cycle-7 Carry-overs
Because cycle 8 added no new packages and bumped none, every cycle-7 dependency finding remains in force unchanged:
### D-AZ795-1 (Low / Hardening) — FluentValidation 12.0.0 → 12.1.1
- Filed in `dependency_scan_cycle7.md` § "FluentValidation 12.0.0" and `security_report_cycle7.md` § "D-AZ795-1".
- Status at cycle-8 tip: **still open**. Cycle 8 did not bump either `FluentValidation` or `FluentValidation.DependencyInjectionExtensions` from 12.0.0 to 12.1.1. The same hardening-release recommendation carries forward.
- Cycle-8-specific re-check at https://github.com/FluentValidation/FluentValidation/security/advisories (audit date 2026-05-23): no NEW advisories published against 12.x since the cycle-7 audit. The bump remains pure forward-compatibility hardening.
### D2-cy4 (Medium / test-runtime only) — `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks`
- Filed in `dependency_scan_cycle4.md` and re-confirmed in cycles 5 and 7.
- Status at cycle-8 tip: **still open**. Cycle 8 did not bump `Microsoft.NET.Test.Sdk`. Test-runtime exposure only; not reachable from the API process.
## Cycle-8 New Source Code Runtime Surface
Because no new packages were added, the new runtime surface introduced by cycle 8 sits **entirely within already-vetted packages**:
| New surface | Hosted by | Risk delta |
|-------------|-----------|------------|
| 4 new `AbstractValidator<T>` subclasses (`RegionRequestValidator`, `CreateRouteRequestValidator`, `GetTileByLatLonQueryValidator`, `UavTileBatchMetadataPayloadValidator`) + 4 helper validators (`RoutePointValidator`, `GeofencePolygonValidator`, `UavTileMetadataValidator`, the inner `GeoCornerValidator`) | `FluentValidation 12.0.0` — same package surface assessed in cycle 7. | None — reflection scan path (`AddValidatorsFromAssemblyContaining<Program>()`) is unchanged from cycle 7; cycle 8 adds more registered types but uses the same registration call. |
| `RejectUnknownQueryParamsEndpointFilter` + `UavUploadValidationFilter` — two new `IEndpointFilter` types | ASP.NET Core 10 — `Microsoft.AspNetCore.Http` already in the BCL footprint. | None — `IEndpointFilter` API surface is unchanged; `UavUploadValidationFilter` is `AddTransient<>`-registered, so per-request instance isolation matches the existing `WithValidation<T>()` pattern from cycle 7. |
| `[JsonRequired]` annotations on 6 modified DTOs (`RequestRegionRequest`, `CreateRouteRequest`, `RoutePoint`, `GeoPoint`, `GeofencePolygon`, `UavTileMetadata`) | `System.Text.Json.Serialization` — BCL. | None — cycle-7 baseline already used `[JsonRequired]` on `TileCoord`; cycle 8 just expands coverage. |
| `[JsonPropertyName]` annotations added by AZ-812 to `RequestRegionRequest.Lat`/`Lon` | `System.Text.Json.Serialization` — BCL. | None. |
## Cycle-8 Findings
**No new dependency findings.**
The cycle-7 D-AZ795-1 Low/Hardening recommendation (`FluentValidation 12.0.0 → 12.1.1`) is **carried forward unchanged** and re-iterated in this cycle's report. Cycle 8 did not regress any prior cycle's posture.
## Verdict
**PASS** (cycle-8 delta) — zero new CVEs, zero new supply-chain blockers, zero new packages.
Cumulative verdict (carrying forward earlier cycles): **PASS_WITH_WARNINGS** — D2-cy4 (cycle-4 Medium, test-runtime only) + D-AZ795-1 (cycle-7 Low/Hardening) both still in effect. Cycle 8 adds nothing to the cumulative dependency-finding ledger.
@@ -0,0 +1,70 @@
# Infrastructure & Configuration Review (Cycle 8)
**Date**: 2026-05-23
**Mode**: Delta scan
**Scope**: Cycle-8 changes to deployment configs, CI/CD files, environment templates, and shell scripts only.
## Cycle-8 Infrastructure-Layer Diff
Computed via `git diff --name-only 865dfdb..b763da3` (cycle-7 tip → cycle-8 tip), filtered to infrastructure-relevant paths:
| File | Diff summary | Security relevance |
|------|--------------|--------------------|
| `Dockerfile`, `Dockerfile.tests`, `Dockerfile.api` (and any image-build file) | **NOT modified** | None — cycle 8 did not touch any image build. |
| `docker-compose.yml`, `docker-compose.tests.yml`, `docker-compose.prod.yml` (and any orchestration file) | **NOT modified** | None — cycle 8 did not touch any compose file. |
| `.woodpecker.yml`, `.github/workflows/**` (and any CI/CD pipeline definition) | **NOT modified** | None — cycle 8 added no automated pipeline changes. |
| `.env`, `.env.example`, `.env.tests` (and any environment template) | **NOT modified** | None — cycle 8 read no new env vars (every cycle-8 validator and filter is pure code; FluentValidation 12.0.0 has no config knobs). |
| `appsettings.json`, `appsettings.Development.json`, `appsettings.tests.json` | **NOT modified** | None — cycle 8 added no new configuration sections. The pre-existing `UavQualityConfig` section is unchanged. |
| `scripts/probe_latlon_validation.sh` | NEW manual probe script for AZ-811 | Reviewed in `static_analysis_cycle8.md` § Test Code Review § `scripts/probe_*_validation.sh`. ✓ |
| `scripts/probe_region_validation.sh` | NEW manual probe script for AZ-808 | Reviewed in `static_analysis_cycle8.md` § Test Code Review § `scripts/probe_*_validation.sh`. ✓ |
| `scripts/probe_route_validation.sh` | NEW manual probe script for AZ-809 | Reviewed in `static_analysis_cycle8.md` § Test Code Review § `scripts/probe_*_validation.sh`. ✓ |
| `scripts/probe_upload_validation.sh` | NEW manual probe script for AZ-810 | Reviewed in `static_analysis_cycle8.md` § Test Code Review § `scripts/probe_*_validation.sh`. ✓ |
| `scripts/run-performance-tests.sh` | Modified — diff is exclusively the AZ-812 wire-format rename (`?Latitude=…&Longitude=…&ZoomLevel=…``?lat=…&lon=…&zoom=…` across query strings, and `{"latitude":…,"longitude":…}``{"lat":…,"lon":…}` across JSON bodies in PT-01 through PT-08 invocations) | No new credentials, no new shell-injection surface. The change is a forced wire-update made necessary by AZ-812's contract rename. ✓ |
| `.vscode/launch.json`, `.vscode/tasks.json`, `README.md` | Modified — developer-tooling artifacts | Out of scope for security audit (not deployed; do not affect runtime). |
## Probe Shell Scripts — Common Safety Posture
All four new `probe_*_validation.sh` scripts follow the same defensive pattern (verified per `static_analysis_cycle8.md`):
- `#!/usr/bin/env bash` with `set -euo pipefail` at the top — fail-fast on undefined vars, broken pipes, command failures.
- `API_URL="${API_URL:-https://localhost:8080}"` default; `JWT="${JWT:-}"` with an explicit `if [[ -z "${JWT}" ]]; then echo "ERROR: set JWT env var to a bearer token. …"; exit 2; fi` guard.
- `curl -k` used (justified — the dev cert is self-signed; the scripts target localhost in dev/test only — documented in each script's header).
- Hand-built JSON payloads via shell variable interpolation — values are integer / quoted strings only, no caller-supplied shell strings.
- No embedded secrets, no hardcoded `$JWT`, no `.env` reads.
Identical posture to the cycle-7 `probe_inventory_validation.sh`. ✓
## Container & Image Security — Carried Forward Unchanged
| Check | Status (carried from cycle 5/6/7) | Cycle-8 impact |
|-------|-----------------------------------|----------------|
| Non-root container user (Dockerfile `USER` directive) | Already in effect | None — no Dockerfile change. |
| Minimal base image (alpine/distroless/etc.) | The API image uses the .NET 10 SDK base — same as cycle 7; image hardening is still owned by a separate, unscheduled follow-up task. | None. |
| No secrets in build args | Verified cycle 5; no `Dockerfile` change in cycle 7 or 8 | None. |
| Health checks | Compose `healthcheck` block on Postgres unchanged | None. |
## CI/CD Security — Carried Forward Unchanged
| Check | Status | Cycle-8 impact |
|-------|--------|----------------|
| Secrets management (env vars / vault, not pipeline literals) | Existing pattern preserved | None. |
| No credentials in pipeline definitions | `.woodpecker.yml` untouched in cycle 8 | None. |
| Artifact signing | Existing posture (none — owned by a separate operational improvement track) | None. |
| Dependency-audit step in pipeline | Existing posture (manual audit per `dependency_scan_cycle*.md`; no automated `dotnet list package --vulnerable` in CI due to the build-hang issue noted in `AGENTS.md`) | None. |
## Environment & Secrets
- `.env.example` — not modified in cycle 8. The cycle-8 code reads no new env vars.
- `appsettings.Development.json` — not modified in cycle 8.
- `appsettings.json` — production template; unchanged in cycle 8.
- The cycle-7 host-port change (`5432:5432``5433:5432`) is preserved through cycle 8; not relevant to production exposure (production containers run on a private docker network without host-port mapping per the existing deployment model).
## Cycle-7 + Earlier Carry-overs
No infrastructure-layer carry-overs from earlier cycles. The cycle-2 `UavQualityConfig`-derived `KestrelServerOptions.Limits.MaxRequestBodySize = 500 MiB` (set in `Program.cs:41-43`) is noted in `owasp_review_cycle8.md` § A05 as a configuration choice that contributed to the F-AZ809-1 exploit-math worst case, but it is not itself an infrastructure-layer finding — the value is correct for the UAV upload endpoint, and the proper fix is per-endpoint size narrowing (a code change, not an infra change).
## Verdict (Phase 4)
**PASS** — zero new infrastructure-layer findings.
The four new probe scripts are dev/test only, env-driven, fail-fast under `set -euo pipefail`, and contain no embedded credentials. The single perf-script update is a wire-rename diff with no security impact. Cycle 8 made no Docker, compose, CI, env-template, or appsettings changes.
+111
View File
@@ -0,0 +1,111 @@
# OWASP Top 10 Review (Cycle 8)
**Date**: 2026-05-23
**Mode**: Delta scan against OWASP Top 10:2021 (current at audit time per https://owasp.org/www-project-top-ten/ — verified 2026-05-23; the 2025 candidate revision is still in public-comment phase and not adopted).
**Scope**: Cycle-8 delta only — AZ-808 (region POST validator), AZ-809 (route POST validator + per-point + per-polygon), AZ-810 (UAV upload metadata validator + custom filter), AZ-811 (lat/lon GET validator + unknown-query-param filter), AZ-812 (region-API `Latitude`/`Longitude``Lat`/`Lon` rename). Earlier cycles' OWASP reviews remain authoritative for their respective surfaces; this file does NOT re-walk the cycle-5 / cycle-7 baselines.
## A01 — Broken Access Control
**Status**: PASS
- `.RequireAuthorization()` is preserved on every cycle-8 endpoint:
- `POST /api/satellite/request` at `Program.cs:251`
- `POST /api/satellite/route` at `Program.cs:267`
- `GET /api/satellite/tiles/latlon` at `Program.cs:213`
- `POST /api/satellite/upload` at `Program.cs:238` (additionally requires the `GPS` permission claim via `SatellitePermissions.UavUploadPolicy`).
- Endpoint-filter execution order is governed by ASP.NET Core's middleware → routing → endpoint-filter pipeline. `app.UseAuthorization()` (line 206) reads the endpoint metadata produced by `.RequireAuthorization()` and short-circuits anonymous callers with 401 BEFORE the endpoint dispatch reaches any endpoint filter. Cycle-8 verification: none of the four new validation paths runs unless the caller is authenticated.
- The new `RejectUnknownQueryParamsEndpointFilter` (lat/lon GET) does not establish its own auth gate — it relies on the endpoint chain's existing `.RequireAuthorization()`. Anonymous query-param probing is impossible.
- No new CORS policy in cycle 8. `TilesCors` (cycle-6 baseline) is unchanged.
- No new IDOR paths — the four endpoints operate on caller-supplied identifiers but do not couple them to any tenant or owner field; tiles remain globally-scoped in the post-AZ-484 model. Geospatial identifiers (`Lat`, `Lon`, `Z/X/Y`) are deterministic projections of physical reality, not capability tokens.
## A02 — Cryptographic Failures
**Status**: N/A (cycle 8)
- Cycle 8 has no cryptographic operations. JWT validation is unchanged from cycle 4 (`AddSatelliteJwt` — HS256 with ≥ 32-byte secret, `ValidateLifetime + ValidateIssuer + ValidateAudience = true`, ClockSkew = 30s).
- The cycle-5 UUIDv5 SHA-1 surface (`Uuidv5.Create`) is unaffected.
- TLS posture (Kestrel `Http1AndHttp2` with self-signed dev cert / ingress termination in prod) — unchanged from cycle 6.
- F-AZ810-2 (`DateTime` vs `DateTimeOffset` parsing) is NOT a crypto failure — it's a time-handling correctness concern documented under A09.
## A03 — Injection
**Status**: PASS
- No SQL / Dapper / Npgsql usage in any cycle-8 new file. (`grep -r 'Dapper\|Npgsql' SatelliteProvider.Api/Validators SatelliteProvider.Api/DTOs SatelliteProvider.Common/DTO` → zero matches across the cycle-8 surface.)
- No `Process.Start` / shell-out / `eval` in any cycle-8 new file.
- All inputs reaching the cycle-8 validators are strongly typed by the time the rules execute (`double`, `int`, `Guid`, `DateTime`, `string`, `IReadOnlyList<T>`). `System.Text.Json` has already parsed and rejected anything malformed before the validator runs, and the cycle-7 deserializer hardening (`UnmappedMemberHandling.Disallow`) is in force on every cycle-8 path including the multipart `metadata` field (`UavUploadValidationFilter.cs:73` uses the same global `JsonSerializerOptions` via `IOptions<JsonOptions>`).
- The cycle-8 wire-format rename (AZ-812) means the deserializer now strictly enforces `lat` / `lon` against the post-rename JSON schema — legacy `Latitude` / `Longitude` fields are rejected as unknown members, surfacing as `JsonException` → 400 via `GlobalExceptionHandler` (not silently bound to a fallback property). This is a small *anti-injection* improvement at the schema boundary.
## A04 — Insecure Design
**Status**: **PASS (post-follow-up)** — was PASS_WITH_WARNINGS at audit time; F-AZ809-1 was the only Medium and was resolved in the Step-14 follow-up commit (cycle 8). Post-follow-up posture is PASS.
- AZ-808 / AZ-809 / AZ-810 / AZ-811 are themselves a *design fix* for the remaining unprotected endpoints — completing the AZ-795 epic's per-endpoint rollout. Pre-cycle-8, four endpoints (region POST, route POST, lat/lon GET, UAV upload) used ad-hoc inline `try/catch` blocks or no input validation at all. Cycle 8 centralises every public endpoint behind one of three approved validation paths:
1. `WithValidation<T>()` for JSON-body endpoints (RegionRequest, CreateRouteRequest).
2. `WithValidation<T>()` + `RejectUnknownQueryParamsEndpointFilter` for query-string endpoints (GetTileByLatLonQuery).
3. Custom `IEndpointFilter` for non-standard wire formats (`UavUploadValidationFilter` for multipart).
- The cycle-7 architecture-doc § 9 coverage table now reaches **100% of public-facing input endpoints** with validators (region POST, route POST, lat/lon GET, inventory POST, UAV upload). Future drift visibility is high.
- **F-AZ809-1** (from `static_analysis_cycle8.md`) identified a design-level gap: `CreateRouteRequestValidator` lacked a max-count cap on `Geofences.Polygons`, in contrast to every other list-bearing field across the API (route `Points` ≤ 500, UAV `Items` ≤ 100, inventory `Tiles`/`LocationHashes` ≤ 5000). **Resolved in the Step-14 follow-up (cycle 8)**: `MaxPolygons = 50` cap added + matching unit + integration tests + `route-creation.md` v1.0.1 Inv-10. The pattern "cap every collection field" is now fully consistent across the API.
## A05 — Security Misconfiguration
**Status**: PASS
- `UnmappedMemberHandling.Disallow` (cycle-7 global default) is now backed by per-endpoint FluentValidation rules at every cycle-8 endpoint, completing the defence-in-depth hardening for mass-assignment prevention.
- Swagger exposure is still gated by `app.Environment.IsDevelopment()` (unchanged).
- `appsettings.Development.json` clearly tags DEV-ONLY JWT iss/aud values; `appsettings.json` ships empty so production fail-fast triggers if env vars are missing (unchanged from cycle 4).
- The new `AddTransient<UavUploadValidationFilter>` registration in `Program.cs:128` is correct — transient (not singleton) ensures each request gets a fresh filter instance, preventing accidental cross-request state retention through filter-instance fields.
- The cycle-7 `AddValidatorsFromAssemblyContaining<Program>()` scope rule is unchanged; the cycle-8 validators all live in `SatelliteProvider.Api.dll`, so the reflection scan correctly picks them up.
- **Note (informational, not a finding)**: the global Kestrel body-size limit (`MaxRequestBodySize = 500 MiB`) was originally set for the UAV upload endpoint in cycle 2 (AZ-488). It applies to every endpoint by default because Kestrel exposes a per-server, not per-endpoint, default. Cycle 8's F-AZ809-1 highlights the consequence: tight per-endpoint count caps in validators are now the primary defence on JSON endpoints, not the framework body limit. A future hardening cycle could narrow the body limit per-endpoint via `IRequestSizeLimitMetadata` on the `RouteHandlerBuilder` for non-upload endpoints — but this is hardening, not a finding.
## A06 — Vulnerable & Outdated Components
**Status**: PASS_WITH_WARNINGS (carry-over Low)
- See `dependency_scan_cycle8.md` for the full table. Summary:
- Cycle 8 added zero new packages and bumped zero existing packages.
- **D-AZ795-1** (cycle-7 carry-over): `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` 12.0.0 → 12.1.1 hardening recommendation — still open. Cycle 8 did not bump.
- **D2-cy4** (cycle-4 carry-over): `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks` Medium — test-runtime exposure only, still open.
## A07 — Identification and Authentication Failures
**Status**: PASS
- JWT validation parameters unchanged from cycle 4 (`AddSatelliteJwt`).
- No new auth-bypass paths introduced by cycle 8. The four new validators / filters cannot run for anonymous callers (see A01).
- The five new integration test files (`CreateRouteValidationTests`, `GetTileByLatLonValidationTests`, `RegionFieldRenameTests`, `RegionRequestValidationTests`, `UavUploadValidationTests`) all mint valid tokens via the shared `JwtTestHelpers.MintAuthenticated(...)` — proves the happy path is properly auth-gated and not relying on any test-only bypass.
- `RejectUnknownQueryParamsEndpointFilter` rejects malicious query keys (e.g. `?debug=1`, `?Authorization=…`) at the endpoint-filter layer — narrows the auth fingerprinting surface vs. the pre-cycle-8 behaviour where unknown keys silently bound to defaults.
## A08 — Software and Data Integrity Failures
**Status**: N/A (cycle 8)
- No CI/CD changes, no artifact-signing changes, no auto-update paths touched in cycle 8. The only CI/script-adjacent file modified is `scripts/run-performance-tests.sh` (wire-rename diff only — see `static_analysis_cycle8.md` § Test Code Review).
## A09 — Security Logging and Monitoring Failures
**Status**: PASS_WITH_WARNINGS (3 Lows — F-AZ810-1 new + F-AZ795-1 + F-AZ795-2 carry-over)
- `GlobalExceptionHandler` 5xx branch logs `Method`, `Path`, `correlationId`, and the exception object (via Serilog default). The 4xx branch does NOT log the exception (intentional, avoids noisy log signal from malformed-payload spam). Unchanged from cycle 7.
- The cycle-8 validators and the new `UavUploadValidationFilter` do NOT add any logging — they return `Results.ValidationProblem(...)` directly without any audit trail. This is consistent with cycle 7's `ValidationEndpointFilter<T>` posture (the response itself carries enough detail for the client to self-debug; the server logs would otherwise drown in 400s from typo-prone callers). Acceptable for the threat model.
- **F-AZ810-1** (NEW in cycle 8): `UavUploadValidationFilter.cs:82` echoes `JsonException.Message` to the client — same information-disclosure pattern as cycle-7 F-AZ795-1 in a NEW code path. Both surfaces leak `System.*` type names + parse positions. **Filed as Low**.
- **F-AZ795-1** (cycle-7 carry-over): `GlobalExceptionHandler.cs:108-117` — still open.
- **F-AZ795-2** (cycle-7 carry-over): `GlobalExceptionHandler.cs:88-93` — still open. Cycle 8 reduces practical reachability on the 4 newly-validated endpoints but doesn't eliminate it.
- **F-AZ810-2** (Informational, listed under A09 as a time-handling correctness concern with downstream logging/monitoring implications): `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` — stale tiles could pass the freshness check in dev environments with non-UTC host TZ if the caller omits the `Z` suffix. Zero observable impact in UTC-deployed production. **Filed as Low / Informational**.
## A10 — Server-Side Request Forgery (SSRF)
**Status**: N/A (cycle 8)
- No URL-input fields, no outbound HTTP calls triggered by the cycle-8 surface. The pre-existing `GoogleMapsDownloaderV2` (outbound calls to Google Maps for tile fetches in the region/route processing paths) is not modified by cycle 8.
- The new `RejectUnknownQueryParamsEndpointFilter` works on the request's already-parsed query collection — no URL parsing, no outbound resolution.
## Cross-Reference with `security_approach.md`
The repo does not contain `_docs/00_problem/security_approach.md` (same as cycle 7). The OWASP review proceeds against the cycle-5 + cycle-6 + cycle-7 architectural decisions documented in `_docs/02_document/architecture.md` § 7 (Security Architecture) and § 9 (Strict wire-format validation at the API edge — added in cycle 7, extended in cycle 8). Cycle 8's input-validation completion cleanly extends those decisions; the F-AZ809-1 gap is the only deviation and is documented as such.
## Verdict (Phase 3)
**PASS_WITH_WARNINGS** (post-follow-up) — A04 (Insecure Design) is now **PASS** (F-AZ809-1 resolved in Step-14 follow-up); A06 + A09 remain `PASS_WITH_WARNINGS` from the carry-over Lows + the new F-AZ810-1 Low. Every other OWASP category is PASS or N/A.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — but cycle 8 resolved its only Medium in-cycle via the Step-14 follow-up commit before any production exposure. Post-follow-up posture is cleaner than the audit-time snapshot.
+125
View File
@@ -0,0 +1,125 @@
# Security Audit Report (Cycle 8)
**Date**: 2026-05-23
**Scope**: Cycle-8 delta over the cycle-7 audit (`_docs/05_security/security_report_cycle7.md`). Cycle-8 surface = AZ-808 (region POST validator) + AZ-809 (route POST validator + per-point + per-polygon) + AZ-810 (UAV upload metadata validator + custom `UavUploadValidationFilter`) + AZ-811 (lat/lon GET validator + `RejectUnknownQueryParamsEndpointFilter`) + AZ-812 (region-API `Latitude`/`Longitude``Lat`/`Lon` wire rename — OSM convention).
**Trigger**: `/autodev` Step 14 (Security Audit) — feature cycle 8, post-implementation, post-test-spec-sync, post-docs-update.
**Verdict (cycle-8 delta)**: **PASS_WITH_WARNINGS** — 0 Medium *open* (F-AZ809-1 **RESOLVED in cycle 8** via the Step-14 follow-up — `MaxPolygons = 50` cap added to `CreateRouteRequestValidator` + matching unit + integration tests) + 2 new Lows open (F-AZ810-1, F-AZ810-2) + 2 cycle-7 Low carry-overs (F-AZ795-1, F-AZ795-2) + 1 cycle-7 dependency Low carry-over (D-AZ795-1) + 1 cycle-4 dependency Medium carry-over (D2-cy4, test-runtime only). Zero Critical / High.
**Verdict (cumulative)**: **PASS_WITH_WARNINGS** — 1 cycle-4 Medium open (D2-cy4, test-runtime only) + 0 cycle-8 Medium open + multiple Lows.
## Summary
| Severity | Cycle 7 delta | Cycle 8 delta (audit-time) | Cycle 8 delta (post-follow-up) | Cumulative open |
|----------|---------------|----------------------------|--------------------------------|-----------------|
| Critical | 0 | 0 | 0 | 0 |
| High | 0 | 0 | 0 | 0 |
| Medium | 0 | 1 (F-AZ809-1) | **0 — F-AZ809-1 RESOLVED in cycle 8** | 1 (D2-cy4 cycle-4 carry — `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks`; test-runtime exposure only) |
| Low | 3 (F-AZ795-1, F-AZ795-2, D-AZ795-1) | 2 new (F-AZ810-1, F-AZ810-2) | 2 new (unchanged — both filed for cycle 9) | 5+ (3 cycle-7 carry + 2 cycle-8 new) |
## OWASP Top 10:2021 Assessment
| Category | Status (cycle-8 delta) | Findings |
|----------|------------------------|----------|
| A01 — Broken Access Control | PASS | — |
| A02 — Cryptographic Failures | N/A | No crypto in cycle 8 |
| A03 — Injection | PASS | — (cycle 8 strengthens — strict deserialization now backed by per-endpoint range checks at all four newly-validated endpoints) |
| A04 — Insecure Design | **PASS (post-follow-up)** — was PASS_WITH_WARNINGS at audit time | F-AZ809-1 **RESOLVED in cycle 8** via Step-14 follow-up: `MaxPolygons = 50` cap added to `CreateRouteRequestValidator` + new Inv-10 in `route-creation.md` v1.0.1 + matching unit + integration tests |
| A05 — Security Misconfiguration | PASS | (Informational note re: global Kestrel 500 MiB body limit; not a finding) |
| A06 — Vulnerable Components | PASS_WITH_WARNINGS | D-AZ795-1 (Low — cycle-7 carry; FluentValidation 12.0.0 → 12.1.1 hardening still available) |
| A07 — Auth Failures | PASS | — (JWT unchanged; every cycle-8 endpoint retains `RequireAuthorization()`) |
| A08 — Data Integrity Failures | N/A | No CI/CD or artifact-signing surface in cycle 8 |
| A09 — Logging Failures | PASS_WITH_WARNINGS | F-AZ810-1 (Low — new instance of F-AZ795-1 pattern in `UavUploadValidationFilter`) + F-AZ795-1 + F-AZ795-2 (Low cycle-7 carry-overs) + F-AZ810-2 (Low / Informational time-handling) |
| A10 — SSRF | N/A | No URL-input fields in cycle 8 |
## Findings
| # | Severity | Category | Location | Title |
|---|----------|----------|----------|-------|
| F-AZ809-1 | Medium (**RESOLVED in cycle 8**) | Insecure Design (A04) | `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82` (pre-fix) | Unbounded `geofences.polygons` collection enables an authenticated DoS on `POST /api/satellite/route` — fixed via `MaxPolygons = 50` cap |
| F-AZ810-1 | Low | Information Disclosure (A09) | `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84` | `JsonException.Message` propagated to client in new code path (parallel to cycle-7 F-AZ795-1) |
| F-AZ810-2 | Low | Time-handling correctness (A09 adjacent) | `SatelliteProvider.Common/DTO/UavTileMetadata.cs:30` + `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60` | `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` — freshness window drifts by host TZ in non-UTC dev environments |
| F-AZ795-1 | Low | Information Disclosure (A09) | `SatelliteProvider.Api/GlobalExceptionHandler.cs:108-117` | (cycle-7 carry) `JsonException.Message` propagated to client in 400 response — still open |
| F-AZ795-2 | Low | Information Disclosure (A09) | `SatelliteProvider.Api/GlobalExceptionHandler.cs:88-93` | (cycle-7 carry) Generic `BadHttpRequestException.Message` propagated as `Detail` for non-JSON 400 paths — still open |
| D-AZ795-1 | Low | Vulnerable & Outdated Components (A06) | NuGet | (cycle-7 carry) `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` 12.0.0 → 12.1.1 (hardening release; no published CVE) — still open |
| D2-cy4 | Medium | Vulnerable & Outdated Components (A06) | NuGet (test runtime) | (cycle-4 carry) `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks` — test-runtime exposure only — still open |
### Finding Details
**F-AZ809-1: Unbounded `geofences.polygons` collection enables an authenticated DoS** (Medium / A04 — Insecure Design) — **RESOLVED in cycle 8 (Step-14 follow-up commit, see git log)**
- Location: `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-92` (post-fix; was lines 72-82 pre-fix).
- Description: The cycle-8 `CreateRouteRequestValidator` chained `RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator())` but originally enforced only `NotEmpty` on the collection — no upper bound on `Geofences.Polygons.Count`. The sibling `Points` collection IS capped at 500; the global `KestrelServerOptions.Limits.MaxRequestBodySize` is set to 500 MiB to accommodate the UAV upload endpoint and applies to every route. With ~90 bytes per minimum-shape polygon JSON, an authenticated caller could have submitted ~5.8 million polygons in a single request; each invalid polygon yields ~3 `ValidationFailure` allocations, for ~17 million `ValidationFailure` objects in worst case — sufficient to saturate the LOH and trigger a full GC pass.
- Impact: Medium. Auth-gated (`RequireAuthorization()` at `Program.cs:267`) — only tenant operators with a valid JWT could reach the endpoint. Within the cycle-8 threat model this was contained; promotion risk eliminated by the cap.
- **Resolution** (Step-14 follow-up): `MaxPolygons = 50` constant added to `CreateRouteRequestValidator.cs`; chained as `.Must(polygons => polygons is null || polygons.Count <= MaxPolygons).WithMessage("…must contain at most 50 polygons.")` on the `geofences.polygons` rule. Cap chosen at 50 because geofences are AOI-restriction rectangles (per `route-creation.md` v1.0.1 Inv-10) — realistic use is 1-10 polygons per route, 50 gives 5x headroom while bounding the validator's worst-case allocation to ~150 `ValidationFailure` objects (well within normal request-handling overhead). Tests added: `CreateRouteRequestValidatorTests.Validate_GeofencePolygonsTooMany_FailsCountRule` (unit) + `CreateRouteValidationTests.GeofencePolygonsTooMany_Returns400` (integration, asserts 51-polygon array → HTTP 400 with `errors["geofences.polygons"]`). Contract bumped to `route-creation.md` v1.0.1 (patch — tightens an existing range; new Inv-10 + test case).
- Status: **resolved**. No cycle-9 follow-up required.
**F-AZ810-1: `JsonException.Message` propagated to client in `UavUploadValidationFilter`** (Low / A09 — Information Disclosure)
- Location: `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84` (the `catch (JsonException ex)` block of the metadata parse)
- Description: The cycle-8 `UavUploadValidationFilter` echoes the raw `JsonException.Message` directly to the client as the value of `errors["metadata"]` — the SAME information-disclosure pattern as cycle-7 F-AZ795-1 (in `GlobalExceptionHandler.cs`), introduced in a second code path that bypasses the global exception handler (the filter intercepts and returns `Results.ValidationProblem(...)` directly).
- Impact: Low. Auth-gated (`.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` at `Program.cs:238`) — only callers holding a valid JWT *with* the `GPS` permission claim can reach the filter. Leaks `System.*` type names + JSON parse positions — content already inferable from the OpenAPI spec.
- Remediation: Sanitise the response message to a generic string (e.g. `"`metadata` could not be parsed as JSON. See the server log for details."`) while continuing to log the raw `ex.Message` server-side under the request's `correlationId`. Lock-step with F-AZ795-1's remediation. Add an integration-test assertion in `UavUploadValidationTests` that no `System.*` substring appears in the response body's `errors[]` value.
- Status: filed for cycle 9 as a child of the same F-AZ795-1 ticket — both call sites get the sanitiser at once.
**F-AZ810-2: `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset`** (Low / Informational — Time-handling correctness)
- Location: `SatelliteProvider.Common/DTO/UavTileMetadata.cs:30` + `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60`
- Description: When `System.Text.Json` deserializes an ISO-8601 string without a `Z` / `+HH:MM` suffix into a `DateTime`, `Kind = Unspecified`. `DateTime.ToUniversalTime()` treats `Unspecified` values as **local time** — the freshness comparison drifts by the host's timezone offset. In a UTC-deployed prod container the local offset is zero; in a developer's local environment with `TZ=Europe/Kyiv` (UTC+02:00 or +03:00) a `capturedAt` value of `"2026-05-22T12:00:00"` would be treated as `2026-05-22T10:00:00Z` in summer, shifting the freshness window by the offset.
- Impact: Low / Informational. Zero observable impact in the supported deployment configuration (Docker containers run in UTC; the cycle-2 `docker-compose.yml` does not override `TZ`). No security exploit: an attacker cannot force the server's TZ. Reported as a defence-in-depth correctness item.
- Remediation (two options): (1) Change the DTO type to `DateTimeOffset` + custom JSON converter rejecting offset-less ISO-8601. (2) Add a FluentValidation rule rejecting `DateTime.Kind == DateTimeKind.Unspecified`. Option 2 is the minimum behaviour-preserving fix; Option 1 is correct for v2.0 of `uav-tile-upload.md`.
- Status: filed for cycle 9 as Low; not release-blocking — every documented client and every integration-test fixture sends the `Z` suffix.
**F-AZ795-1 / F-AZ795-2 / D-AZ795-1**: see `_docs/05_security/security_report_cycle7.md` § "Finding Details" — all three carry-overs are unchanged at cycle-8 tip. The cycle-8 F-AZ810-1 finding is a NEW instance of the F-AZ795-1 pattern in a different code path; both surfaces must be sanitised in lock-step.
**D2-cy4**: see `_docs/05_security/dependency_scan_cycle4.md` — unchanged at cycle-8 tip. Test-runtime exposure only.
## Dependency Vulnerabilities
| Package | CVE | Severity | Fix Version | Status |
|---------|-----|----------|-------------|--------|
| FluentValidation 12.0.0 | — (hardening only) | Low | 12.1.1 | D-AZ795-1 cycle-7 carry — still open |
| FluentValidation.DependencyInjectionExtensions 12.0.0 | — (hardening only) | Low | 12.1.1 | D-AZ795-1 cycle-7 carry — still open (same item) |
| Microsoft.NET.Test.Sdk 17.8.0 (transitive `NuGet.Frameworks`) | — (cycle-4 carry) | Medium | TBD (next Test SDK refresh cycle) | D2-cy4 cycle-4 carry — still open |
No new dependency vulnerabilities in cycle 8. Cycle 8 added zero new packages.
## Recommendations
### Immediate (Critical/High)
None.
### Short-term (Medium)
1. ~~**Add `MaxPolygons` cap to `CreateRouteRequestValidator`** (F-AZ809-1)~~**DONE in Step-14 follow-up (cycle 8)**. Cap set at 50; see Finding Details § F-AZ809-1 § Resolution.
### Long-term (Low / Hardening)
2. **Sanitise client-visible 400 messages** (F-AZ795-1 + F-AZ795-2 + F-AZ810-1) — single sanitiser applied to BOTH `GlobalExceptionHandler.WriteClientErrorAsync` AND `UavUploadValidationFilter.InvokeAsync`. Pre-existing-class instance in `UavTileUploadHandler.cs:80` must also be sanitised at the same time so the defence-in-depth path doesn't continue leaking. Add matching test assertions in `TileInventoryValidationTests` + `UavUploadValidationTests`. Estimated 2 hours. File as a small follow-up child of AZ-795 (epic).
3. **Bump FluentValidation 12.0.0 → 12.1.1** (D-AZ795-1) — single `.csproj` edit + regression test pass. No API surface change in 12.0.0 → 12.1.1 per the upstream changelog.
4. **Add `DateTimeKind.Unspecified` rejection rule to `UavTileMetadataValidator`** (F-AZ810-2 Option 2) — single `.Must(capturedAt => capturedAt.Kind != DateTimeKind.Unspecified)` rule. Doesn't break any documented client (all examples send `Z` suffix). One-line code change + matching unit test.
5. **Per-endpoint Kestrel body-size narrowing** (informational hardening) — apply `.WithMetadata(new RequestSizeLimitMetadata { MaxRequestBodySize = … })` to each non-upload endpoint so the 500 MiB global is constrained for JSON endpoints (e.g. 1 MiB for region/route, 10 MiB for inventory). Reduces F-AZ809-1's exploit surface even after the polygon cap is applied. Tracker child of AZ-795 or a standalone hardening task.
### Pre-existing-class follow-up (Low / Informational)
6. **Drop the `try/catch (ArgumentException)` block in the `CreateRoute` handler** (`Program.cs:387-399`) — pre-cycle-8 inconsistency that cycle 8 reduced the reachability of but did not eliminate. The validator now intercepts most `ArgumentException` cases; the catch leaks `ex.Message` in a non-conformant `{error: string}` shape. Fold into the same cycle-9 follow-up as #2 above.
### Cumulative reminders (carry-overs)
- **D2-cy4** — `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks` Medium, test-runtime only. Owned by the next Test SDK refresh.
## Cycle-8 Architectural Wins
The audit specifically wants to record four improvements introduced this cycle:
1. **Per-endpoint input-validation completion** — the AZ-795 strict-validation epic introduced in cycle 7 reached **100% coverage** in cycle 8. Every public-facing input endpoint (region POST, route POST, lat/lon GET, inventory POST, UAV upload) now runs a validator behind the same `error-shape.md` v1.0.0 contract. Future drift visibility is high; the architecture doc's coverage table is now complete.
2. **Three approved validation paths formalised** — the architecture doc (`_docs/02_document/architecture.md` § 9) now codifies three approved validation paths: `WithValidation<T>()` for JSON-body endpoints; `WithValidation<T>()` + `RejectUnknownQueryParamsEndpointFilter` for query-string endpoints; custom `IEndpointFilter` for non-standard wire formats. Future endpoints have a clear pattern to follow; an audit can verify any new endpoint via the chosen-path criterion.
3. **Wire-format normalisation under strict mode** (AZ-812) — `Latitude` / `Longitude``Lat` / `Lon` on the region endpoint aligns with OSM convention used everywhere else in the API. Under `UnmappedMemberHandling.Disallow` (cycle 7 global), legacy `Latitude` / `Longitude` payloads are now rejected at the deserializer with a structured 400 — no silent coercion, no compatibility-shim layer. This is a hard breaking change for any consumer still on the old shape, but it is intentional and documented as the post-cycle-8 contract.
4. **Defence-in-depth retained for the UAV upload handler**`UavTileUploadHandler.HandleAsync` retains every envelope check it had pre-cycle-8 (`metadata` presence, JSON parse, `Items.Count == 0`, `Items.Count != files.Count`, `Items.Count > MaxBatchSize`). The new `UavUploadValidationFilter` is *additive* — it intercepts the primary endpoint path but does not weaken the service-layer guard for direct callers (e.g. unit tests). The two layers' shape choices are mutually compatible.
## Verdict
**PASS_WITH_WARNINGS** (post-follow-up) — 0 Medium open (F-AZ809-1 resolved in Step-14 follow-up) + 2 cycle-8 Lows + 3 cycle-7 Low carry-overs + 1 cycle-4 Medium carry-over (test-runtime only). Zero Critical / High.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — but cycle 8 fixed its only Medium in-cycle, leaving only the cycle-4 test-runtime carry-over Medium open. Cycle 8 is **safe to release** within its documented threat model (authenticated callers only on every endpoint). The post-follow-up posture is *cleaner* than the cycle-7 baseline because cycle 8 added zero new open Mediums and resolved its in-cycle finding before commit.
Cumulative posture: PASS_WITH_WARNINGS (1 cycle-4 Medium open carry-over + 0 cycle-8 Medium open + multiple Lows). Cycle 8 completed an architecturally important hardening epic (100% input-validation coverage) without leaving Medium or higher debt.
+241
View File
@@ -0,0 +1,241 @@
# Static Analysis (Cycle 8)
**Date**: 2026-05-23
**Mode**: Delta scan
**Scope**: Source code introduced or changed by AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812. Cycle-7 baseline (`static_analysis_cycle7.md`) remains authoritative for the AZ-794 / AZ-795 / AZ-796 surface; this scan only audits the cycle-8 delta.
**Files in scope** (40 changed source files; non-test detail):
- **API — 11 files**
- `SatelliteProvider.Api/Program.cs` (DI + endpoint wiring deltas — `WithValidation<RequestRegionRequest>`, `WithValidation<CreateRouteRequest>`, `WithValidation<GetTileByLatLonQuery>`, `AddEndpointFilter<UavUploadValidationFilter>`, `AddTransient<UavUploadValidationFilter>`, `RejectUnknownQueryParamsEndpointFilter` registration)
- `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs` (new — record with nullable bindings)
- `SatelliteProvider.Api/Validators/RegionRequestValidator.cs` (new — AZ-808)
- `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs` (new — AZ-809)
- `SatelliteProvider.Api/Validators/GeofencePolygonValidator.cs` (new — AZ-809)
- `SatelliteProvider.Api/Validators/RoutePointValidator.cs` (new — AZ-809)
- `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs` (new — AZ-810)
- `SatelliteProvider.Api/Validators/UavTileBatchMetadataPayloadValidator.cs` (new — AZ-810)
- `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs` (new — AZ-810)
- `SatelliteProvider.Api/Validators/GetTileByLatLonQueryValidator.cs` (new — AZ-811)
- `SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs` (new — AZ-811)
- `SatelliteProvider.Api/Swagger/ParameterDescriptionFilter.cs` (AZ-811 — added `lat`/`lon`/`zoom` description entries)
- **Common — 6 DTO files** (AZ-808/809/810/812 — `[JsonRequired]` annotations + AZ-812 rename)
- `SatelliteProvider.Common/DTO/RequestRegionRequest.cs`
- `SatelliteProvider.Common/DTO/CreateRouteRequest.cs`
- `SatelliteProvider.Common/DTO/GeofencePolygon.cs`
- `SatelliteProvider.Common/DTO/GeoPoint.cs`
- `SatelliteProvider.Common/DTO/RoutePoint.cs`
- `SatelliteProvider.Common/DTO/UavTileMetadata.cs`
- **Test code** (reviewed for fixture-only secrets + auth-bypass patterns) — 8 new validator unit tests + 4 new integration test files + several modified integration test helpers.
- **Shell scripts** (reviewed for embedded secrets + unsafe sequences) — 4 new probe scripts (`probe_latlon_validation.sh`, `probe_region_validation.sh`, `probe_route_validation.sh`, `probe_upload_validation.sh`) + 1 modified perf script (`run-performance-tests.sh`, wire-rename diff only).
**Method**: Read each new file end-to-end; targeted `Grep` for injection / hardcoded-credential / unsafe-API patterns (`password|secret|api.?key|bearer|token` over `SatelliteProvider.Api/Validators` returned **0 matches**); diff-review of every DTO change vs. its cycle-7 baseline; trace each `[JsonRequired]` chain through to its FluentValidation rule.
## Findings
### F-AZ809-1 — Unbounded `geofences.polygons` collection enables an authenticated DoS via `CreateRouteRequest` (Medium / A04 — Insecure Design) — **RESOLVED in cycle 8 (Step-14 follow-up)**
- **Location**: `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82` (`When(req => req.Geofences is not null, () => …)`).
- **Description**: The `CreateRouteRequestValidator` chains a `RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator())` block but only enforces `NotEmpty` on the collection. There is **no upper bound on `Geofences.Polygons.Count`**. The parent collection `Points` IS capped (`MaxPoints = 500` at line 27) — the polygons collection is the only nested list-bearing field on this endpoint without a cap.
- **Code**:
```csharp
When(req => req.Geofences is not null, () =>
{
RuleFor(req => req.Geofences!.Polygons)
.NotNull().WithMessage("`geofences.polygons` is required when `geofences` is present.")
.NotEmpty().WithMessage("`geofences.polygons` must contain at least 1 polygon when `geofences` is present.")
.OverridePropertyName("geofences.polygons");
RuleForEach(req => req.Geofences!.Polygons)
.SetValidator(new GeofencePolygonValidator())
.OverridePropertyName("geofences.polygons");
});
```
- **Exploit math** (worst-case envelope under current configuration):
- `KestrelServerOptions.Limits.MaxRequestBodySize = uavBatchBodyLimit = MaxBatchSize × MaxBytes = 100 × 5 MiB = 500 MiB` (set globally in `Program.cs:41-43` for the UAV endpoint; the same limit applies to every endpoint by default because Kestrel exposes a per-server, not per-endpoint, default).
- Minimum JSON polygon footprint: `{"northWest":{"lat":1.0,"lon":2.0},"southEast":{"lat":3.0,"lon":4.0}}` ≈ 90 bytes including the comma separator.
- Theoretical maximum polygon count in a single 500 MiB request: `500 MiB / 90 bytes ≈ 5.8 million polygons`.
- With invalid polygons (e.g. `lat` out of range), `GeofencePolygonValidator` adds 2 corner-range `ValidationFailure` objects + 1 cross-field NW-of-SE failure = ~3 failures per polygon. Worst-case allocation: ~17 million `ValidationFailure` instances + the matching error-map keys before the filter formats the `ValidationProblemDetails` body.
- **Impact**: Medium. A single authenticated authorized request can saturate the LOH (large object heap) and trigger a full GC pass on the API process. The endpoint is `RequireAuthorization()`-gated (line 251 in `Program.cs`) so anonymous callers cannot reach the validator — the attacker must hold a valid JWT. But once authenticated, a single malformed request degrades the service for every other tenant operator until GC reclaims the heap. Repeatable. No data leak, no privilege escalation; pure availability impact.
- **OWASP mapping**: A04 — Insecure Design (missing rate/size limit on a collection-bearing input field). Adjacent to A05 (Security Misconfiguration — the global Kestrel limit was set for the UAV endpoint in cycle 5 but applies to every endpoint).
- **Remediation**: Add `Must(p => p is null || p.Count <= MaxPolygons)` with a defensible upper bound. Reasonable cap candidates:
- `MaxPolygons = 50` (consistent with the historical use case — a route is unlikely to need more than a handful of geofence rectangles for AOI restriction).
- `MaxPolygons = MaxPoints = 500` (consistent with the sibling `Points` cap on the same DTO).
- The matching `cumulative_review_batches_01-04_cycle8_report.md` already enumerates `points.Count <= 500` (route), `items.Count <= 100` (UAV upload), `coords.Count <= 1000` (tile inventory, cycle 7) as bounded — the missing entry for `geofences.polygons` is the one inconsistency.
- **Status**: **resolved in cycle 8 (Step-14 follow-up)**. Cap added: `MaxPolygons = 50` constant + `.Must(polygons => polygons is null || polygons.Count <= MaxPolygons).WithMessage("…must contain at most 50 polygons.")` on the `geofences.polygons` rule. Cap chosen at 50 because geofences are AOI-restriction rectangles — realistic use is 1-10 polygons per route, 50 gives 5x headroom while bounding the validator's worst-case allocation to ~150 `ValidationFailure` objects. Tests added: `CreateRouteRequestValidatorTests.Validate_GeofencePolygonsTooMany_FailsCountRule` (unit) + `CreateRouteValidationTests.GeofencePolygonsTooMany_Returns400` (integration). Contract bumped to `route-creation.md` v1.0.1 (patch — tightens an existing range; new Inv-10 + test case). See `security_report_cycle8.md` § F-AZ809-1 § Resolution for the full disposition.
### F-AZ810-1 — `JsonException.Message` propagated to client in `UavUploadValidationFilter` (Low / A09 — Information Disclosure)
- **Location**: `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84` (the `catch (JsonException ex)` block of the metadata parse).
- **Code**:
```csharp
catch (JsonException ex)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
[MetadataField] = new[] { $"`metadata` could not be parsed as JSON: {ex.Message}" },
});
}
```
- **Description**: The cycle-8 `UavUploadValidationFilter` echoes the raw `JsonException.Message` directly to the client as the value of `errors["metadata"]`. This is the **same information-disclosure pattern as cycle-7 F-AZ795-1** (in `GlobalExceptionHandler.cs:108-117`), introduced in a *second* code path that bypasses the global exception handler (the filter intercepts and returns `Results.ValidationProblem(...)` directly). Cycle-7 F-AZ795-1 remains open; cycle 8 adds a second instance of the same pattern that would also need to be sanitised by the same remediation.
- **Impact**: Low. Same severity classification as F-AZ795-1. Auth-gated (`.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` at `Program.cs:238`) — only callers holding a valid JWT *with* the `GPS` permission claim can reach the filter and trigger this path. The leaked content (type names, parse positions, `System.Text.Json` fingerprint) is already inferable from the OpenAPI spec; the new path narrows the attack surface for an authenticated GPS-permissioned operator but does not expose secrets, PII, or pivot vectors.
- **Remediation**: Sanitise the response message to a generic string (e.g. `"`metadata` could not be parsed as JSON. See the server log for details."`) while continuing to log the raw `ex.Message` server-side under the request's `correlationId`. Best done in tandem with F-AZ795-1's remediation since both paths surface the same exception class through the same response shape. Add an integration-test assertion in `UavUploadValidationTests` that no `System.*` substring appears in the response body's `errors[]` value, mirroring the cycle-7 gap noted in `static_analysis_cycle7.md` § F-AZ795-1 *Test coverage gap*.
- **Status**: open — file as a cycle-9 follow-up child of the same F-AZ795-1 ticket so both call sites get the sanitiser at once.
### F-AZ810-2 — `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` (Low / Informational — Time-handling correctness)
- **Location**: `SatelliteProvider.Common/DTO/UavTileMetadata.cs:30` + `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60`.
- **Code**:
```csharp
// UavTileMetadata.cs:30
[JsonRequired]
public DateTime CapturedAt { get; init; }
// UavTileMetadataValidator.cs:52-60
RuleFor(m => m.CapturedAt)
.Must(capturedAt => capturedAt.ToUniversalTime() <= tp.GetUtcNow().UtcDateTime.AddSeconds(futureSkewSeconds))
.WithMessage($"`capturedAt` must be within {futureSkewSeconds}s of the current time (no future-dated tiles).")
.Must(capturedAt => capturedAt.ToUniversalTime() >= tp.GetUtcNow().UtcDateTime.AddDays(-maxAgeDays))
.WithMessage($"`capturedAt` must be within the last {maxAgeDays} days.");
```
- **Description**: When `System.Text.Json` deserializes an ISO-8601 string into a `DateTime`, the resulting `DateTime.Kind` depends on the string's offset suffix:
- `"2026-05-22T12:00:00Z"``Kind = Utc`.
- `"2026-05-22T12:00:00+03:00"``Kind = Local` after normalization to local time.
- `"2026-05-22T12:00:00"` (no suffix) → `Kind = Unspecified`.
For `Kind = Unspecified`, `DateTime.ToUniversalTime()` treats the value as **local time**, which means the freshness comparison drifts by the host's timezone offset. In a UTC-deployed prod container (Linux `TZ=UTC`), the local offset is zero and there is no observable impact. In a developer's local environment (e.g. `TZ=Europe/Kyiv` = UTC+02:00 or +03:00), a `capturedAt` value of `"2026-05-22T12:00:00"` would be treated as `2026-05-22T10:00:00Z` (in summer), shifting the freshness window by the offset.
- **Impact**: Low / Informational. Zero observable impact in the supported deployment configuration (Docker containers run in UTC by default; the cycle-2 `docker-compose.yml` does not override `TZ`). The freshness rule could be loosely-bounded by the same offset (in either direction) in a dev environment with a non-UTC host TZ. No security exploit: an attacker cannot force the server's TZ; they can only submit `capturedAt` values, and the server's UTC-deployed configuration treats those deterministically.
- **OWASP mapping**: A09 — Security Logging and Monitoring Failures (adjacent — a stale freshness window could mask an out-of-band attack on the upload endpoint).
- **Remediation** (two options):
1. **Strict**: Change the DTO type to `DateTimeOffset` so the parsed value always carries an explicit offset, and add a JSON converter that rejects offset-less ISO-8601 strings at deserialization (`JsonConverterAttribute` pointing at a custom converter that checks for the `Z` / `+HH:MM` suffix and throws `JsonException` if missing — surfaces as a 400 via `GlobalExceptionHandler`).
2. **Lenient**: Add a FluentValidation rule that rejects `DateTime.Kind == DateTimeKind.Unspecified` so the caller must supply a tz-aware ISO-8601 string. Keeps the DTO shape; doesn't break clients that already send `"Z"` suffix.
Option 2 is the minimum behaviour-preserving fix. Option 1 is correct for a v2.0 of `uav-tile-upload.md`.
- **Status**: open — file as a Low cycle-9 follow-up. Not release-blocking for cycle 8 because every documented client in `uav-tile-upload.md` example payloads and every integration-test fixture sends the `Z` suffix.
## Pattern Sweep — Cycle-8 Delta
### Injection (SQL / Command / XSS / Template)
| Pattern | Result |
|---------|--------|
| `string.Format`, interpolation `$"..."`, or concatenation feeding into a Dapper / Npgsql command in the new files | None. The 9 new files in `SatelliteProvider.Api/Validators/` and `SatelliteProvider.Api/DTOs/` do not touch the data layer. |
| `Process.Start`, `subprocess`, `eval`, `Invoke-Expression`, raw `system()` | None. |
| User-input echoed into HTML (XSS) | None. The API returns JSON only. The cycle-8 `RejectUnknownQueryParamsEndpointFilter` echoes the offending parameter name back as a JSON string value — `System.Text.Json` performs canonical JSON escaping of control characters; no HTML injection vector. |
| Template injection (Razor / Liquid / etc.) | None. No templating in the new files. |
### Authentication & Authorization
| Pattern | Result |
|---------|--------|
| Hardcoded credentials, secrets, API keys | `Grep -i 'password|secret|api.?key|bearer|token'` against `SatelliteProvider.Api/Validators/` returned **0 matches**. The new integration test files (`CreateRouteValidationTests`, `GetTileByLatLonValidationTests`, `RegionRequestValidationTests`, `UavUploadValidationTests`, `RegionFieldRenameTests`) reuse the runner-side `JwtTestHelpers.MintAuthenticated(...)` helper for token attachment; no hardcoded secret material. |
| Missing `.RequireAuthorization()` on a public endpoint | Every cycle-8 endpoint binding in `Program.cs` retains `.RequireAuthorization()`: `RequestRegion` (line 251), `CreateRoute` (267), `GetTileByLatLon` (213), `UploadUavTileBatch` (238 — additionally requires the `GPS` permission claim via `SatellitePermissions.UavUploadPolicy`). |
| Validator running before auth check | No. ASP.NET Core endpoint filters run AFTER the routing layer's authorization middleware (`app.UseAuthorization()` at `Program.cs:206`). All four cycle-8 filters (`ValidationEndpointFilter<RequestRegionRequest>`, `ValidationEndpointFilter<CreateRouteRequest>`, `RejectUnknownQueryParamsEndpointFilter` + `ValidationEndpointFilter<GetTileByLatLonQuery>`, `UavUploadValidationFilter`) cannot run for anonymous callers — the 401 short-circuit fires first. |
| Permission/policy regression | `RequiresGpsPermission` on `/api/satellite/upload` retained. `RejectUnknownQueryParamsEndpointFilter` does NOT call `RequireAuthorization()` itself — it relies on the endpoint chain's existing `.RequireAuthorization()` (line 213). Correct. |
### Cryptographic Failures
| Pattern | Result |
|---------|--------|
| Weak hash (MD5 / SHA1) used for passwords or signatures | None in cycle 8. Pre-existing UUIDv5 SHA-1 surface (`Uuidv5.Create`) is untouched. |
| New crypto material introduced | None. Cycle 8 has no cryptography. |
| Plaintext transmission | API listens on `https://+:8080` with ALPN (cycle-6 baseline, unchanged). |
### Data Exposure
| Pattern | Result |
|---------|--------|
| Sensitive data in logs | The new validators and filters do NOT log the request body. `GlobalExceptionHandler` 5xx branch logs `Method`, `Path`, `correlationId`, and the exception object (pre-existing). Cycle 8 doesn't widen this. |
| Sensitive fields in API responses | F-AZ810-1 (`JsonException.Message` echo) above is the only new echo-back path. No password hashes, no PII (the four endpoints carry only metadata: geospatial coords, integer IDs, timestamps). The `RejectUnknownQueryParamsEndpointFilter` echoes the offending parameter NAME (not value) back — and the list of allowed param names IS already in the OpenAPI spec, so no new fingerprinting surface. |
| Debug endpoints in production | Swagger is gated by `app.Environment.IsDevelopment()` (unchanged). |
| Secrets in version control | `.env*` files are gitignored. The 4 new probe scripts (`probe_*_validation.sh`) all use `${JWT:?…}`-style env-var reads with explicit "set JWT env var" error guards; no embedded credentials. |
### Insecure Deserialization
| Pattern | Result |
|---------|--------|
| `Pickle` / `BinaryFormatter` / unsafe XML / `JsonConvert.DeserializeObject<T>` with `TypeNameHandling.All` | None in cycle 8. `UavUploadValidationFilter.cs:73` uses `JsonSerializer.Deserialize<UavTileBatchMetadataPayload>(...)` with the global `JsonSerializerOptions` from `IOptions<JsonOptions>` — which has `UnmappedMemberHandling.Disallow` set by cycle-7 `ConfigureHttpJsonOptions`. ✓ |
| Unbounded collection sizes | **F-AZ809-1 above**`geofences.polygons` lacks a cap. Other cycle-8 list fields ARE capped: `Points.Count <= 500` (`CreateRouteRequestValidator:60-61`), `Items.Count <= MaxBatchSize=100` (`UavTileBatchMetadataPayloadValidator:27-28`). Cycle-7 `Tiles.Count <= 5000` / `LocationHashes.Count <= 5000` unchanged. The framework-level `MaxRequestBodySize` bound is in force for all paths — but it's set at 500 MiB to accommodate the UAV upload endpoint, which makes per-endpoint count caps the primary defence. |
### Integer Overflow / Bounded Math
The cycle-8 validators do not perform any integer arithmetic beyond `Math.Max(options.ValueLengthLimit, uavQuality.MaxBatchSize * 512)` (`Program.cs:61``100 * 512 = 51_200`, no overflow possible). No left-shifts, no power-of-two computations on caller-controlled values. ✓
### ReDoS / Algorithmic Complexity
Cycle-8 validation rules are all O(1) per entry × N entries with bounded N (with the F-AZ809-1 exception above):
| Validator | Per-entry cost | Total cost (worst case) |
|-----------|----------------|-------------------------|
| `RegionRequestValidator` | O(1) — 5 range checks on scalar fields | O(1) |
| `CreateRouteRequestValidator` (root + cross-field invariant) | O(1) base + O(N points) + O(P polygons unbounded) | **O(N × P)** with unbounded P — see F-AZ809-1 |
| `RoutePointValidator` (via `RuleForEach`) | O(1) — 2 range checks | O(500) bounded by parent `MaxPoints` |
| `GeofencePolygonValidator` (via `RuleForEach`) | O(1) — 2 corner null + 4 range + 2 cross-field | O(P) where P is unbounded — see F-AZ809-1 |
| `UavTileMetadataValidator` (via `RuleForEach`) | O(1) — 4 range checks + 2 freshness | O(100) bounded by `MaxBatchSize` |
| `UavTileBatchMetadataPayloadValidator` (root) | O(1) | O(1) |
| `GetTileByLatLonQueryValidator` | O(1) — 3 cascaded NotNull + range | O(1) |
| `RejectUnknownQueryParamsEndpointFilter` | O(K log K) where K = caller's query-param count | O(K) bounded by Kestrel's per-request header/query limits (default 32 KB of query string) |
No regex, no recursion, no nested loops with caller-controlled bounds (except F-AZ809-1).
## `UavUploadValidationFilter` Defence-in-Depth Verification
The filter intercepts the primary multipart-upload code path. The pre-existing `UavTileUploadHandler.HandleAsync` retains the SAME envelope checks (`SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs:64-141`) — confirming the filter is **defence-in-depth on top of** the handler's checks, not a replacement:
| Check | `UavUploadValidationFilter` (filter layer) | `UavTileUploadHandler` (handler layer — defence-in-depth) |
|-------|--------------------------------------------|-----------------------------------------------------------|
| `metadata` form field present + non-empty | Line 62-68 | Line 68-71 (`string.IsNullOrWhiteSpace`) |
| `metadata` parses as JSON | Line 71-84 (catches `JsonException`) | Line 74-81 (catches `JsonException`) — **same pattern, same `ex.Message` echo (pre-existing parallel to F-AZ810-1)** |
| `metadata.items` non-null + non-empty | Line 86-92 (`payload is null`) + `UavTileBatchMetadataPayloadValidator:25-26` (`NotNull`/`NotEmpty`) | Line 83-86 (`Items.Count == 0`) |
| `metadata.items.Count == files.Count` | Line 105-118 | Line 88-91 |
| `metadata.items.Count <= MaxBatchSize` | `UavTileBatchMetadataPayloadValidator:27-28` | Line 93-96 |
The handler's envelope checks are still reachable by any caller invoking `IUavTileUploadHandler.HandleAsync(...)` directly (e.g. unit tests, or a future programmatic flow). Cycle 8 left them intact — correct ✓. The duplicated `JsonException.Message` echo in the handler (line 80) is a pre-existing-class instance of the same Low finding as F-AZ810-1; the remediation MUST sanitise both call sites in lock-step or the defence-in-depth path will continue to leak.
## Pre-existing-Surface Inconsistency Noted (NOT a cycle-8 regression)
`Program.cs:387-399` — the `CreateRoute` handler still wraps its body in `try { ... } catch (ArgumentException ex) { return Results.BadRequest(new { error = ex.Message }); }`. This is a pre-cycle-8 inconsistency: the response shape is the unstructured `{error: string}` object, NOT the `ValidationProblemDetails` shape mandated by `error-shape.md` v1.0.0. Cycle 8 added `WithValidation<CreateRouteRequest>` at line 268 which intercepts most `ArgumentException` cases — so the `catch` block is now largely dead code, but it WILL fire if `IRouteService.CreateRouteAsync` itself throws `ArgumentException` (e.g. a future internal validation rule). When it fires, the response leaks `ex.Message` in a non-conformant shape.
- **Severity**: Low / Informational. Pre-existing inconsistency; cycle 8 reduced its reachable surface but did not eliminate it.
- **Remediation**: Drop the `try/catch` block now that the validator covers the same cases at the filter layer; let unexpected `ArgumentException` propagate to `GlobalExceptionHandler` for uniform 400 + sanitised `ValidationProblemDetails` (or 500 with a `correlationId` if the cause is internal). One-line edit; recommend folding into the same cycle-9 follow-up as F-AZ795-1 / F-AZ810-1 since all three converge on the same error-shape consistency improvement.
## Test Code Review
### `SatelliteProvider.Tests/Validators/*ValidatorTests.cs` (8 new files)
- Pure CPU; no I/O, no network, no file system, no DB.
- All inputs constructed inline. No fixture-file reads, no hardcoded JWTs.
- All test files share the cycle-7 `ValidatorTestModuleInitializer.cs` (`[ModuleInitializer]``GlobalValidatorConfig.ApplyOnce()` at test-assembly load) — single source of truth for the camelCase property resolver. No test drift risk.
- ✓ No findings.
### `SatelliteProvider.IntegrationTests/{CreateRouteValidationTests,GetTileByLatLonValidationTests,RegionFieldRenameTests,RegionRequestValidationTests,UavUploadValidationTests}.cs` + modified helpers
- All five new files use the runner-side `JwtTestHelpers.MintAuthenticated(...)` to attach a Bearer token, mirroring cycle 7's pattern. No hardcoded secret material; the token's signing secret comes from the `JWT_SECRET` env var (32+ bytes, dev-only in `docker-compose.tests.yml`).
- Test inputs use raw `HttpRequestMessage` with hand-built JSON strings — exercises the exact wire shape the validator + deserializer see in production. Each file covers the cycle-8 negative cases for its endpoint.
- `ProblemDetailsAssertions.cs` was extended (additive) — no removed assertions. The cycle-7 `error-shape.md` Inv-2 / Inv-4 contract assertions all still apply.
- `UavUploadTests.cs` + `UavUploadValidationTests.cs` both clamp their fixture-generated coordinates into non-overlapping OSM-valid sub-ranges (cycle-8 hotfix commit `b763da3` per `_docs/03_implementation/batch_04_cycle8_report.md` § AC-9) — a test-data correctness fix, not a security finding.
- ✓ No findings.
### `scripts/probe_{latlon,region,route,upload}_validation.sh` (4 new files)
- All four scripts begin with `set -euo pipefail` — fail-fast on undefined vars, broken pipes, command failures.
- All four read `${API_URL:-https://localhost:8080}` (default to localhost) and `${JWT:-}` with an explicit "ERROR: set JWT env var" guard that `exit 2`s if `$JWT` is empty. No embedded credentials.
- `curl -k` is used (justified — the dev cert is self-signed; the scripts target localhost in dev/test only — documented in each script's header).
- ✓ No findings — same posture as cycle-7's `probe_inventory_validation.sh`.
### `scripts/run-performance-tests.sh` (modified, wire-rename only)
- Diff is exclusively the AZ-812 wire-format rename: `?Latitude=…&Longitude=…&ZoomLevel=…``?lat=…&lon=…&zoom=…` and `{"latitude":…,"longitude":…}``{"lat":…,"lon":…}` across PT-01 through PT-08 invocations. No new code, no new credentials, no shell-injection surface introduced.
- ✓ No findings.
## Cycle-7 Carry-overs (still open at cycle-8 tip)
| Finding | Source cycle | Carry-over status | Cycle-8 interaction |
|---------|--------------|-------------------|---------------------|
| **F-AZ795-1**`JsonException.Message` propagated in `GlobalExceptionHandler.cs:108-117` | cycle 7 | open | Unchanged. Cycle 8 introduced **F-AZ810-1** which is the SAME pattern in a NEW code path; both need lock-step sanitiser remediation. |
| **F-AZ795-2** — Generic `BadHttpRequestException.Message` propagated in `GlobalExceptionHandler.cs:88-93` | cycle 7 | open | Unchanged. Cycle 8 routes more endpoint failures through `WithValidation<T>()` which builds `ValidationProblemDetails` directly — reducing the practical reachability of this path on the 4 newly-validated endpoints, but not eliminating it (model-binding failures pre-validator can still hit the path). |
## Verdict (Phase 2)
**PASS_WITH_WARNINGS** (post-follow-up) — 0 Medium open (F-AZ809-1 resolved in cycle 8) + 2 new Lows (F-AZ810-1, F-AZ810-2) + 2 cycle-7 Low carry-overs (F-AZ795-1, F-AZ795-2). No Critical or High findings.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — but cycle 8 resolved its only Medium in-cycle via the Step-14 follow-up commit before any production exposure. The 2 new Lows + 2 carry-over Lows are not release-blockers in isolation; they are filed for the next cycle's follow-up batch.
@@ -0,0 +1,75 @@
# Perf Run — Cycle 8 (AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812)
**Date**: 2026-05-23T12:50Z (first run) + 2026-05-23T12:53Z (re-run after PT-06 script fix)
**Run label**: cycle8 — full default-parameter run after the cycle-8 strict-validation sweep (AZ-808 region POST + AZ-809 route POST + AZ-810 UAV upload metadata + AZ-811 GET tile lat/lon + AZ-812 region OSM rename) and the in-cycle F-AZ809-1 polygon-cap follow-up (commit `8fca6e0`).
**Trigger**: autodev existing-code Step 15 (Performance Test gate). Cycle 8 goal: confirm that adding FluentValidation + `JsonUnmappedMemberHandling.Disallow` to four endpoints — and the new 50-polygon cap on `POST /api/satellite/route` — introduced no measurable regression on existing perf scenarios.
**Runner**: `scripts/run-performance-tests.sh` (default params: `PERF_REPEAT_COUNT=20`, `PERF_UAV_BATCH_SIZE=10`). Two runs were needed; see "PT-06 script fix" below.
**System under test**: `docker compose up -d --build` against `mcr.microsoft.com/dotnet/aspnet:10.0`; api healthy on `https://localhost:18980` (TLS+ALPN, dev cert `./certs/api.crt` trusted via `--cacert`). Postgres on `localhost:5433`. Single docker-compose stack lifecycle across both runs (no restart between them).
**Build**: `SatelliteProvider.IntegrationTests` Release built inside the SDK; 0 errors / 15 warnings (carried-over NU1902 IdentityModel — already tracked as D-AZ795-1 / Low / Hardening — plus CA2227 setter-on-collection — already noted, design choice for DTO mutability).
**JWT**: minted by `SatelliteProvider.IntegrationTests --mint-only` (canonical `JwtTokenFactory` surface per AZ-491); 4 h lifetime, 341 bytes.
## Results
| # | Scenario | Verdict | Observed | Threshold | Source of measurement | Source of threshold |
|---|----------|---------|----------|-----------|-----------------------|---------------------|
| PT-01 | Tile download (cold) | **PASS** | 885 ms | ≤ 30000 ms | run 1 | `_docs/02_document/tests/performance-tests.md` |
| PT-02 | Cached tile retrieval | **PASS** | 244 ms | ≤ 500 ms | run 1 | `_docs/02_document/tests/performance-tests.md` |
| PT-03 | Region 200 m / z18 | **PASS** | 99 ms | ≤ 60000 ms | run 1 | `_docs/02_document/tests/performance-tests.md` |
| PT-04 | Region 500 m / z18 + stitch | **PASS** | 2128 ms | ≤ 120000 ms | run 1 | `_docs/02_document/tests/performance-tests.md` |
| PT-05 | 5 concurrent regions | **PASS** | 2663 ms | ≤ 300000 ms | run 1 | `_docs/02_document/tests/performance-tests.md` |
| PT-06 | Route creation (2 points) | **PASS** (after fix) | 83 ms | ≤ 5000 ms | run 2 (post-script-fix; see below) | `_docs/02_document/tests/performance-tests.md` |
| PT-07 | Region request distribution (N=20, cold + warm) | **PASS** | cold p50=2113 ms, p95=2274 ms · warm p50=52 ms, p95=108 ms | warm p95 < cold p95 (delta 2166 ms) | run 1 — only run with a true cold cache for the PT-07 coord band; see "Known harness quirks" below | AZ-484 / AZ-492 |
| PT-08 | UAV batch upload (batch=10, N=20) | **PASS** | batch p50=106 ms, p95=379 ms; per-item proxy p95=37 ms; accepted=200, rejected=0, failed=0 | batch p95 ≤ 2000 ms (AZ-488) | run 1 | `_docs/02_document/tests/performance-tests.md` |
**Raw verdict: 8 Pass · 0 Warn · 0 Fail · 0 Unverified** (after the PT-06 script-fix re-run).
## PT-06 script fix (AZ-809 contract-sync follow-up)
Run 1 reported `PT-06: HTTP 400 (expected 200)`. Root cause: the perf script's `POST /api/satellite/route` body omitted `requestMaps` and `createTilesZip`. AZ-809 (cycle 8) made both fields required — no defaulting — under FluentValidation's `RuleFor(...).NotNull()` chain (see `BT-29` rule 10 in `_docs/02_document/tests/blackbox-tests.md` and `route-creation.md` v1.0.1 Inv-10 / Rule 10). The perf script had been updated for AZ-812's lat/lon wire rename during cycle 8 but missed AZ-809's newly required fields — a contract-sync miss, not a production regression.
Fix: one line in `scripts/run-performance-tests.sh` PT-06 body — added `"requestMaps":false,"createTilesZip":false`. Re-run confirmed `PT-06: 83 ms` (well under the 5000 ms threshold, comparable to cycle-7's 161 ms). Both fields are `false` so the rule-12 cross-field (`createTilesZip=true requires requestMaps=true`) is not exercised — the scenario's intent is route-creation latency, not the cross-field gate.
## AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 NFR verification
Cycle 8 added per-endpoint strict validation (FluentValidation + `JsonUnmappedMemberHandling.Disallow`) to four endpoints. The directly relevant scenarios are PT-03 / PT-04 / PT-05 (region POST — exercises AZ-808 validator on every accept), PT-06 (route POST — exercises AZ-809 validator), PT-07 (region POST distribution — exercises AZ-808 validator 40 times), and PT-08 (UAV upload — exercises AZ-810 validator 200 times via the 20×10 batch). PT-01 / PT-02 exercise AZ-811 (GET tile lat/lon) on every call.
**Validator cost**: invisible at the percentile resolutions reported. Each cycle-8 validator iterates O(N) bounds checks on a small input (typical N ≤ 20 for routes / regions / UAV batches), each check is constant-time, and the FluentValidation rule cache amortises the rule-tree across requests. The measured p95 deltas vs cycle 7 are all within noise band (see "Trend vs cycle 7" below).
**F-AZ809-1 polygon-cap fix (commit `8fca6e0`)**: the new `MaxPolygons = 50` check is a single `Count <= 50` comparison on the chained `RuleFor(...).Must(...)`; it runs in O(1) and is invisible at the PT-06 resolution (83 ms). No new perf scenario was added for the cap because the cap's intent is allocation-bounding under adversarial load, not steady-state latency — that load profile would belong in a separate adversarial/abuse scenario, deferred (see "Follow-ups").
**Auth-before-validation ordering**: confirmed in `Program.cs` (`UseAuthentication()` / `UseAuthorization()` run before any `WithValidation()` endpoint filter and before `UavUploadValidationFilter`). PT-06 / PT-07 / PT-08 calls carry the Bearer token; an unauthenticated probe would 401 before any validator runs, so the validator cost is bounded by authenticated traffic only.
## Trend comparison vs cycle 7
| Scenario | Cycle 7 | Cycle 8 | Δ | Cause |
|----------|---------|---------|---|-------|
| PT-01 cold | 998 ms | 885 ms | -113 ms | noise band (Google Maps DNS / cold-network variance) |
| PT-02 cached | 269 ms | 244 ms | -25 ms | noise |
| PT-03 region 200 m | 139 ms | 99 ms | -40 ms | noise (both runs warm-cache hits on the PT-02 / PT-03 shared coord) |
| PT-04 region 500 m + stitch | 2110 ms | 2128 ms | +18 ms | noise |
| PT-05 5 concurrent | 3145 ms | 2663 ms | -482 ms | noise band (queue scheduler variance under 5-way concurrency) |
| PT-06 route create | 161 ms | 83 ms | -78 ms | noise band (TLS connection state); post-fix value |
| PT-07 cold p95 / warm p95 | 2608 ms / 76 ms | 2274 ms / 108 ms | cold -334 ms / warm +32 ms | warm uptick is noise (≈40 % of the cycle-7 warm-p95 absolute number — both within sub-200 ms noise band on dev hardware); cold improved as the network state stabilised. Delta cold→warm remains > 2000 ms — same order of magnitude as cycle 7 (2532 ms). |
| PT-08 batch p95 | 284 ms | 379 ms | +95 ms | noise band (single-client p95 over 20 batches on shared dev hardware; the cycle 7 number was on the low end of the historical distribution — cycle 5 measured 350 ms, cycle 6 measured 544 ms, cycle 8 sits in the middle of that band). |
**Zero scenarios show a meaningful regression attributable to cycle 8.** All deltas are within the historical noise band for dev hardware (PT-08 has the widest historical band: 284544 ms across cycles 5/6/7/8).
## Known harness quirks (pre-existing — not cycle-8 regressions)
These surfaced during this run but are NOT caused by cycle 8. Each is documented here for trend-tracking visibility; remediation is a perf-harness-cleanup track, not a cycle-8 deliverable.
- **PT-07 cross-run cache pollution**: `PT07_BASE_LAT` / `PT07_BASE_LON` are hard-coded constants (47.471747, 37.657063) with deterministic per-iteration offsets. Back-to-back perf runs against the same docker-compose stack lifecycle reuse the same coord band, so the second run's "cold pass" is actually a warm-cache hit (p95 dropped from 2274 ms in run 1 to 70 ms in run 2; warm p95 also 70 ms; the script's strict `<` check then fails because they're equal). Cycle-7's report (§ "Trend comparison") implicitly noted the same family of issue for PT-03. Fix candidate: parameterise the base coord by a per-run nonce or `PERF_RUN_LABEL` env so each cycle's PT-07 starts cold.
- **PT-01 "cold" misnomer**: `PT01_LAT` / `PT01_LON` (47.461347, 37.646663) are hard-coded; the tile has been cached on the host filesystem since cycle 5 (`./tiles/18/...`). The reported number is "first-request latency on a stack-lifecycle-fresh API process," not a true Google-Maps round-trip. The 885 ms is comfortably under the 30 s threshold because the threshold was set for the genuine cold case; the measurement nonetheless under-reports cold-path latency. Fix candidate same as PT-07: parameterise by nonce.
- **PT-03 cached-by-PT-02 side effect** (cycle 7's note): PT-03 reuses the (47.461747, 37.647063) coord PT-02 already populated; this is by design (PT-03's threshold is 60 s end-to-end, so even a fully cold region would pass, and the test's intent is region-orchestration overhead, not tile-fetch latency).
## Follow-ups (not blocking deploy)
1. **Perf-harness cleanup** — parameterise `PT07_BASE_*` and `PT01_*` coords by a run-nonce env (e.g., `PERF_RUN_LABEL`) so back-to-back runs and trend comparisons are not corrupted by cross-run cache state. Tracker entry candidate (sized ~2 SP).
2. **F-AZ809-1 cap adversarial scenario** — add an explicit PT-NN that posts 50 polygons (the cap) and 51 polygons (one over), measures validator latency and 400-response time. This converts the cap's intent (DoS-bound) into a measurable regression-gate. Deferred to cycle 9 (sized ~3 SP — adds one perf scenario + threshold + report row).
3. **PT-09 promotion**`tile-inventory.md` already notes the option to promote `TileInventoryTests.PerformanceBudget_AC4` from full-suite to `scripts/run-performance-tests.sh § PT-09` if the budget tightens. Not needed at the cycle-8 budget level; track for cycle 10+.
## Verdict (Step 15)
**PASS** — 8/8 scenarios within threshold (after the trivial AZ-809 contract-sync fix to PT-06's body). Zero meaningful regressions attributable to cycle 8. Cycle-8 strict-validation overhead is below percentile resolution on every measured endpoint.
Cleared to auto-chain to Step 16 (Deploy).
+284
View File
@@ -0,0 +1,284 @@
# Retrospective — Cycle 8 (2026-05-23)
**Tasks**: AZ-812 (Region API field rename `Latitude/Longitude → Lat/Lon`, OSM convention, 3 SP), AZ-808 (Region POST strict validation, 3 SP), AZ-811 (lat/lon GET strict validation, 3 SP), AZ-809 (Route POST strict validation + nested DTO chain, 5 SP), AZ-810 (UAV upload metadata strict validation, multipart envelope, 3 SP). **5 tasks, 17 SP, 4 batches.**
**Mode**: cycle-end (autodev Step 17). Step 16 (Deploy) and Step 16.5 (Release) skipped per user choice (A at both gates) — matches the cycle-1-to-7 historical pattern (`_docs/04_deploy/` empty across all 8 cycles; `_docs/04_release/` likewise). The pushed commits on `origin/dev` (tip `32bc5c1`) are the cycle's release record.
**Previous retro**: `retro_2026-05-22_cycle7.md`
**Cycle shape**: explicit follow-through on cycle 7's Action 3 (AZ-795 child-task sweep across the remaining public endpoints) — 4 of the 4 endpoints cycle 7 named (`POST request`, `POST route`, `POST upload`, `GET tiles/latlon`) shipped a strict-validation surface in cycle 8. First time the project has shipped a **directly-traceable cross-cycle improvement action** end-to-end (cycle-7 retro recommendation → cycle-8 task slate → cycle-8 closure).
## 1. Implementation Metrics
| Metric | Cycle 8 | Δ vs cycle 7 |
|--------|---------|--------------|
| Tasks implemented | **5** (AZ-808, AZ-809, AZ-810, AZ-811, AZ-812) | +2 |
| Batches executed | **4** | +3 |
| Avg tasks / batch | **1.25** | -1.75 |
| Total complexity delivered | **17 SP** (3+3+3+5+3) | +9 SP |
| Avg complexity / batch | **~4.25 SP** | -3.75 SP |
| Tasks at-or-below 5 SP cap | **5 of 5 (100%)** | unchanged |
| Tasks split mid-cycle | **0** | unchanged |
| Tasks above cap | 0 | unchanged |
| Cumulative reviews | **1** (`cumulative_review_batches_01-04_cycle8_report.md` — trigger fired at batch 3 per the "every 3 batches" rule, executed after batch 4 closure) | +1 |
| Cross-cycle leftovers carried in | **0** | unchanged |
| Cross-cycle leftovers carried out | **0** | unchanged |
| Implementation report artifact written | **YES** (`implementation_report_strict_validation_cycle8.md`) **+ completeness report + cumulative review** | **PATTERN 1 RESOLVED** (cycle 7 had none) |
**Sequencing rationale**: 4 batches sized 1-2 tasks each (vs cycle 7's single 3-task batch). The split reflects the spec structure: AZ-812 (rename) was a prerequisite for the validator wave because it changed the wire format the validators would enforce; AZ-808 + AZ-811 paired because they shared the same wiring pattern (`.WithValidation<T>()` JSON-body + a query-param filter); AZ-809 stood alone because its 3-DTO-deep nested validator chain (route + per-point + per-polygon-corner) was meaningfully more complex than batches 2/4; AZ-810 stood alone because its multipart-envelope filter was a different shape from the JSON-body endpoints. The 4-batch slicing kept each batch under the "5 SP per batch" implicit complexity ceiling and made the per-batch reviews tractable.
Cycle 8 completed in **8 dev commits** on `dev` (and 1 commit on a pre-cycle prep): the four implementation commits (one per batch — batches 1-3 were committed in their own autodev runs, batch 4 in the closing run), then `bbe8783` (Cycle 8 close + AZ-810 fix-test-coords adjacent), `b763da3` (AZ-810 clamp UAV test fixture coords), then the post-implementation sync chain: `ec0eb90` (test-spec sync, Step 12), `6207ab7` (docs sync, Step 13), `ac40a8b` (security audit, Step 14), `8fca6e0` (F-AZ809-1 fix, Step 14 follow-up), `32bc5c1` (perf run, Step 15). All five trailing commits pushed together at the end of Step 15 per the user's Step 16-gate choice. **+1 commit vs cycle 7** (8 vs 7) — accounted for by the F-AZ809-1 in-cycle resolution commit (cycle 7 had no in-cycle security-finding resolution).
## 2. Quality Metrics
### Code Review Results
| Verdict | Count | Percentage |
|---------|-------|-----------|
| PASS | **0** | 0% |
| PASS_WITH_NOTES | **2** (batches 02 + 03) | 50% |
| PASS_WITH_WARNINGS | **2** (batches 01 + 04) | 50% |
| FAIL | 0 | — |
| **Cumulative review** | **PASS_WITH_WARNINGS** (cycle-8 implementation phase approved for closure) | n/a |
First cycle with a per-batch review file written for every batch (cycle 7 had no per-batch review files; cycle 6 had one per batch but only one batch). The cumulative cross-batch review surfaced no new finding categories beyond the per-batch reviews — i.e., the per-batch reviews caught everything.
### Findings by Severity (cycle-8 review surface)
| Severity | Cycle 8 (code review) | Cycle 8 (security audit) | Combined | Δ vs cycle 7 |
|----------|----------------------|---------------------------|----------|--------------|
| Critical | 0 | 0 | 0 | unchanged |
| High | 0 | 0 | 0 | unchanged |
| Medium | 0 | **1 resolved in-cycle** (F-AZ809-1) + 1 carry-over (D2-cy4) | 1 (resolved) + 1 carry-over | new resolved-in-cycle pattern |
| Low | **4** (1 per batch — all DRY-in-test-helpers or design-decision documented) | 2 new (F-AZ810-1 + F-AZ810-2) + 3 carry-over (F-AZ795-1 + F-AZ795-2 + D-AZ795-1) | 6 new + 3 carry-over | +1 vs cycle 7's 3 |
| Info | **2** (batches 02 + 04 — both documented and accepted) | 0 | 2 | new pattern (cycle 7 had no Info findings) |
| Remaining post-cycle | **6 Low** all open (4 code-review test-DRY items + 2 security Lows from F-AZ810-1/2); 1 Medium resolved (F-AZ809-1); 1 Medium carry-over (D2-cy4) unchanged; 3 Low carry-over (F-AZ795-1/2 + D-AZ795-1) unchanged | | | +3 Lows vs cycle 7's 3 |
### Findings by Category (cycle-8 review surface — code-review side only)
| Category | Count | Top Files |
|----------|-------|-----------|
| Maintainability | **3 Low** (test-DRY) | `SatelliteProvider.IntegrationTests/ProblemDetailsAssertions.cs` (batch-1 promotion → resolved batch 2); `SatelliteProvider.Tests/Validators/UavTileMetadataValidatorTests.cs` (`FixedTimeProvider` duplicated 4×); `SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs` (`PostBatch` duplicated 2×) |
| Bug | **0** | — |
| Spec-Gap | **0** | — |
| Security | **0** (code-review side) — security audit findings counted separately | — |
| Performance | **0** | — |
| Style | **0** | — |
| Scope | **0** | unchanged |
| Design-Decision Documented | **1 Low (batch 03)** | `SatelliteProvider.Api/Validators/RoutePointValidator.cs``OverridePropertyName` chain ordering documented inline to prevent future "simplification" |
| Info (rationale documented) | **2** | `SatelliteProvider.Api/DTOs/GetTileByLatLonQuery.cs` (nullable types rationale, batch 02); `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs` metadata-key wire shape (batch 04) |
### Security audit (cycle 8)
| Metric | Value | Δ vs cycle 7 |
|--------|-------|--------------|
| Verdict | **PASS_WITH_WARNINGS** (after in-cycle F-AZ809-1 resolution) | unchanged (cycle 7 also PASS_WITH_WARNINGS) |
| Reason | F-AZ809-1 Medium (DoS via unbounded `geofences.polygons`) resolved in-cycle via `MaxPolygons = 50` cap + unit + integration test + `route-creation.md` v1.0.1 patch bump (commit `8fca6e0`). F-AZ810-1 + F-AZ810-2 (Lows) carried forward as cycle-9 candidates. | new resolved-in-cycle pattern |
| New Critical / High | **0** | unchanged |
| New Medium | **1 found, 1 resolved in-cycle** (F-AZ809-1) | first time a security-audit finding was resolved within the cycle it was discovered |
| New Low | **2** (F-AZ810-1 `JsonException.Message` echo in `UavUploadValidationFilter`; F-AZ810-2 `DateTime` vs `DateTimeOffset` for `CapturedAt`) | -1 (cycle 7 had 3 new Lows) |
| Carry-overs (still OPEN, unchanged from cycle 7) | **4** (F-AZ795-1 + F-AZ795-2 + D-AZ795-1 + D2-cy4) | +1 (D2-cy4 explicitly re-flagged as carry-over in this cycle's dependency scan) |
**Audit shape**: cycle 8 ran the full 5-phase security skill (Dependency Scan, Static Analysis, OWASP Review, Infrastructure Review, Security Report) — second consecutive cycle to run all 5 phases (cycle 7 was the first). Audit was justified by 4 new validator files + 4 new endpoint contracts + 1 new endpoint filter + 1 new multipart envelope filter. **Cycle 8 is the first cycle where a security-audit Medium finding (F-AZ809-1) was resolved within the same cycle it was discovered** (commit `8fca6e0`, ~30 minutes from discovery to fix landed). The user-driven A/B/C gate at Step 14 explicitly enabled this; the autodev orchestrator now treats in-cycle finding resolution as a first-class option rather than always deferring to cycle N+1.
### Performance gate (cycle 8)
| Metric | Value |
|--------|-------|
| Verdict | **PASS** (after in-cycle perf-script contract fix; see §4 Pattern 3) |
| Scenarios | **8 Pass · 0 Warn · 0 Fail · 0 Unverified** — PT-01..PT-08 |
| Script exit code | **0** (third consecutive clean exit-0 — cycle 6, 7, 8) |
| AZ-808..AZ-812 contract regression check | **PASS** — every cycle-8 validator surface measured below percentile resolution; PT-06 (route create) initially failed with HTTP 400 because the perf script omitted the cycle-8 newly-required `requestMaps` and `createTilesZip` fields (one-line script fix landed in the same commit as the perf report — `32bc5c1`). The miss was a perf-script contract sync gap — the script had been updated for AZ-812's `lat`/`lon` rename but not for AZ-809's new required fields. |
| Cycle-8 validator overhead | **invisible** at percentile resolution. PT-03/04/05/07 (region POST) measurements all within noise band vs cycle 7. PT-06 (route POST) at 83 ms (post-fix) vs cycle 7's 161 ms — both in the noise band. PT-08 (UAV upload) at 379 ms batch p95 vs cycle 7's 284 ms — within historical band (cycle 5-7 range: 284-544 ms). |
| F-AZ809-1 polygon-cap fix overhead | **invisible** — the new `MaxPolygons = 50` check is a single `Count <= 50` comparison; the cap's intent is allocation-bounding under adversarial load, not steady-state latency, and a dedicated adversarial scenario is recommended for cycle 9 (recorded in `perf_2026-05-23_cycle8.md` § Follow-ups). |
| Cross-cycle leftover handling | **0 new leftovers** in `_docs/_process_leftovers/`. The PT-06 sync-miss was fixed inline (Action 3-style mechanical fix), not deferred. |
## 3. Structural Metrics (cycle-8 delta — first snapshot since cycle 5)
| Metric | Cycle 8 | Δ vs cycle 5 (last snapshot) |
|--------|---------|-------------------------------|
| Project count | **9** | unchanged |
| Cross-project ProjectReference edges | **21** | unchanged (3 consecutive cycles with zero new edges) |
| Cycles in project import graph | **0** | unchanged (5 consecutive cycles clean DAG) |
| Max in-degree | Common at **7** | unchanged |
| Max out-degree | Tests at **7** | unchanged |
| Migrations | 15 (cycle 5: 14) | +1 (added by cycle 6 — AZ-505 leaflet index; no migrations in cycles 7 or 8) |
| Contracts in `_docs/02_document/contracts/api/` | **6** (error-shape + region-request + route-creation + tile-inventory + tile-latlon + uav-tile-upload) | +5 vs cycle 5 (cycle 5 had only `uav-tile-upload`; cycles 6/7/8 added the rest) |
| **Cycle 8 alone**: new contracts | **3** (region-request v1.0.0 + route-creation v1.0.0→v1.0.1 + tile-latlon v1.0.0) | new pattern: cycle 8 more than doubled the cycle-5 contract count |
| **Cycle 8 alone**: contract MINOR bumps | **1** (uav-tile-upload v1.1.0 → v1.2.0) | — |
| **Cycle 8 alone**: contract PATCH bumps | **1** (route-creation v1.0.0 → v1.0.1, F-AZ809-1 follow-up) | new pattern: first patch bump on a same-cycle contract |
| **Cycle 8 alone**: contract MAJOR bumps | 0 | — |
| Contract coverage of documented endpoints | **5 / 7** = **71%** | up from cycle 7's 2 / 7 = 29% (cycle 8 closed the documented-but-uncontract gap for region + route + lat/lon) |
| New NuGet packages | **0** | unchanged (3 consecutive zero-bump cycles) |
| `SatelliteProvider.Api/Validators/` file count | **13** | +9 (cycle 8 added 9 new validator files; cycle 7 had added the 4 foundation files) |
| New unit-test methods in cycle 8 | **63** (across 8 new test files in `SatelliteProvider.Tests/Validators/`) | — |
| New integration-test methods in cycle 8 | **52** (across 5 new test files in `SatelliteProvider.IntegrationTests/*ValidationTests.cs`) | — |
| New probe scripts in cycle 8 | **4** (probe_region_validation.sh + probe_latlon_validation.sh + probe_route_validation.sh + probe_upload_validation.sh) | — |
| Net Architecture delta | **-1** | first negative delta since cycle 4 (Pattern 1 from cycle 7 closed; F-AZ809-1 Medium resolved in-cycle so net contribution 0; only Lows carry forward) |
Detail: `_docs/06_metrics/structure_2026-05-23_cycle8.md`.
**Cycle 6 + 7 structure snapshot gap**: cycles 6 and 7 did not write a `structure_*.md`. The cycle-8 snapshot is computed vs cycle 5 (the most recent), so the cycle-6 + cycle-7 deltas are folded into the cycle-8 row. **Process recommendation**: codify the structure-snapshot write as a Step-17 hard requirement in `.cursor/skills/retrospective/SKILL.md` Step 1 self-verification (currently soft-checked).
## 4. Trend Comparison (cycle 7 → cycle 8)
| Trend | Direction | Notes |
|-------|-----------|-------|
| Tasks per cycle | **+2** (3 → 5) | Cycle 8 was the AZ-795 child-task sweep — 4 endpoint validators + 1 prerequisite rename. The increased task count is by design. |
| Complexity per cycle | **+9 SP** (8 → 17) | Largest single-cycle SP delivery in project history. |
| Batches per cycle | **+3** (1 → 4) | Per-batch slicing kept each batch ≤ 5 SP and made per-batch reviews tractable (cycle 7 was 1 batch × 8 SP — heavier review per artifact). |
| Findings volume (post-review) | **+3 Low** (3 → 6) | Code-review Lows are all test-helper DRY (cycle-9 follow-up: promote `FixedTimeProvider` + `PostBatch` to `SatelliteProvider.TestSupport`). Security audit added 2 new Lows + 1 Medium **resolved in-cycle**. |
| Code-review pass rate | **n/a → 100% PASS_WITH_*** (4 / 4 batches reviewed; 0 FAILs) | First cycle with a per-batch review file for every batch; cycle 7 had none. |
| Leftovers carried out | **unchanged** (0 → 0) | Third consecutive cycle with zero new process leftovers (cycle 6 was the first). |
| Architectural cycles introduced | **0** (unchanged) | 5 consecutive cycles with a clean DAG. |
| Contract artifacts produced | **+3** (1 → 4: 3 new + 1 minor bump + 1 patch) | Cycle 8 published 3 new contracts (region-request + route-creation + tile-latlon) and bumped `uav-tile-upload` v1.1.0 → v1.2.0 + `route-creation` v1.0.0 → v1.0.1 (in-cycle F-AZ809-1 follow-up). |
| Migrations | **0** (unchanged 0 → 0) | Second consecutive migration-free cycle. |
| Step 14 (Security Audit) outcome | **PASS_WITH_WARNINGS** (unchanged — cycle 7 was also PASS_WITH_WARNINGS) | First cycle where a Medium finding was resolved in-cycle (F-AZ809-1 → commit `8fca6e0`). |
| Step 15 (Performance Test) script exit | **0** (unchanged) | Third consecutive clean exit-0; required one in-cycle perf-script fix for the AZ-809 contract sync miss. |
| Cross-cycle bug-introduction pattern | **0 new** | Nothing newly broken this cycle. The strict-validation surface continued to surface latent bugs (`UavUploadTests.NextTestCoordinate` lat-out-of-range — see Pattern 2 below — same family as cycle 7's `IdempotentPostTests` discovery), but they're pre-existing bugs surfaced by the new strict layer, not cycle-8 regressions. |
| Mid-cycle scope decisions (split / defer / re-spec) | **0** | No mid-cycle pivots; all 5 tasks shipped as planned across 4 batches. |
| Post-merge correction commits | **+1** (0 → 1) | Cycle 8 had one in-cycle fix commit (F-AZ809-1) AND one perf-script fix folded into the perf-report commit. Both are policy-driven (security-audit and perf-gate findings respectively), not error-driven (vs cycle 6's AC-5 TLS pivot which was a planning miss). |
| Implementation report artifact | **PRESENT** | Closes cycle-7 Pattern 1. Implementation report + completeness report + cumulative cross-batch review all written. |
| Per-batch review files | **4** (one per batch) | Closes cycle-7 process gap on per-batch reviews. Cycle 7 had 0 review files; cycle 8 has 4. |
| In-cycle security-finding resolution | **NEW** (F-AZ809-1) | First cycle to resolve a security-audit Medium within the cycle it was discovered. |
**Cumulative LESSONS reuse**: 5 lessons from previous cycles were directly applicable this cycle:
- **2026-05-22 process lesson on implement-skill artifact contract (cycle 7)** — directly closed by cycle 8: every cycle-8 batch wrote a per-batch report AND a per-batch review file, the cumulative cross-batch review fired (`cumulative_review_batches_01-04_cycle8_report.md`), the implementation phase ended with a consolidated `implementation_report_strict_validation_cycle8.md` AND a `implementation_completeness_cycle8_report.md`. Four artifact types where cycle 7 had two. The lesson's recommendation B (codify the fallback) was not formally written into the skill files — instead, the implement skill self-corrected by producing the artifacts. Either approach satisfies the underlying need.
- **2026-05-22 testing lesson on strict-validation surfaces latent bugs (cycle 7)** — directly applied this cycle: `UavUploadTests.NextTestCoordinate` had been seeding `(Ticks/TicksPerSecond) % 1_000_000` as the latitude offset since cycle 2, which produced values up to 160° (far above 90°). AZ-488's happy-path test never noticed because there was no lat/lon validator at the upload endpoint. AZ-810 added the validator and the cycle-8 batch-4 smoke run caught the bug. Fix was a 2-file clamp (UavUploadTests + UavUploadValidationTests use non-overlapping clamped ranges to preserve per-source UNIQUE-index safety). Same family as cycle 7's `IdempotentPostTests` discovery — the strict-validation surface continues to surface latent test-fixture bugs that the prior lenient defaults masked.
- **2026-05-23 process lesson on no-regression AC verification (cycle 8 batch 4)** — added inline by the implement skill during the AZ-810 batch-4 batch report (already in the ring buffer from a prior commit, dated 2026-05-23). Directly invoked the next day during the cycle-8 retro Step 1 self-verification.
- **2026-05-12 process lesson on Deferred-NFR ring-buffer (cycle 2)** — cycle 8 reused: the F-AZ809-1 finding was resolved in-cycle rather than deferred to cycle 9, preventing it from becoming a multi-cycle item.
- **2026-05-12 dependencies lesson on Major-version bumps (cycle 4)** — cycle 8 reused (negatively): cycle 8 declined to take the cycle-7 D-AZ795-1 FluentValidation 12.0.0 → 12.1.1 bump per scope discipline. The lesson's "verify transitive drift" precondition was satisfied (D-AZ795-1's static analysis re-confirmed no behavioural change in 12.1.1).
## 5. Patterns and Insights
### Pattern 1 — Cycle 7 Action 3 (AZ-795 child-task sweep) ships end-to-end in cycle 8 (NEW positive pattern)
Cycle 7's retro § 5 Action 3 recommended a per-endpoint child-task sweep under AZ-795 to add strict validation to `POST /api/satellite/request`, `POST /api/satellite/route`, `POST /api/satellite/upload`, and `GET /api/satellite/tiles/latlon`. **Cycle 8 shipped all four** — AZ-808 + AZ-809 + AZ-810 + AZ-811 — plus the prerequisite AZ-812 OSM rename that was a coordination-clause dependency.
**Why this matters**: this is the first cycle in the project's history where a cycle-N retrospective recommendation was both (a) tracker-captured as new PBIs in cycle N+1's spec slate and (b) shipped end-to-end in cycle N+1. Prior cycles had carry-overs (e.g., the cycle-3 perf-harness leftover that lingered until cycle 6), but those were single-task or single-fix items. Cycle 7's recommendation was a 4-task sweep + 1 coordination prerequisite, and cycle 8 delivered all 5 as a coherent cycle theme.
**Insight**: retrospective recommendations have measurable impact when (a) the recommendation explicitly names concrete tracker tickets / files / endpoints (cycle 7's Action 3 named the 3-4 endpoints + the SP sizing), (b) the next cycle's planning phase pulls the recommendation directly into the task slate without re-deriving scope, and (c) the recommendation is sized as a coherent cycle theme rather than a one-off fix. Cycle 7's Action 3 satisfied all three. Cycle 8's retrospective should treat this as the gold standard for recommendation phrasing.
### Pattern 2 — Strict-validation surface surfaced one more latent test-fixture bug (continuing positive pattern from cycle 7)
`UavUploadTests.NextTestCoordinate` had a latent bug since cycle 2: the seed `lat = (Ticks/TicksPerSecond) % 1_000_000` produced lat values far above 90° (at iteration n=200_000 → lat=160°). The bug never fired because (a) AZ-488 had no validator at the upload endpoint, (b) no DB constraint on lat range, (c) the test only checked HTTP 200 (which the unvalidated endpoint returned). AZ-810 added the validator and the cycle-8 smoke run caught it. The 2-file clamp fix landed in batch 4 alongside AZ-810 (`UavUploadTests` to [50,70) lat × [10,40) lon; `UavUploadValidationTests` to [-70,-50) × [-40,-10) for per-source UNIQUE-index safety).
**Why this matters**: cycle 7's identical pattern was `IdempotentPostTests` posting `{"Lat":..., "Lon":...}` against a `[JsonPropertyName("lat")]` DTO. Cycle 8's `NextTestCoordinate` bug is the second instance in two consecutive cycles. **The strict-validation rollout is a multi-cycle latent-bug excavation** — every new validator surface uncovers another bug that lenient defaults had masked.
**Insight**: when a strict-validation child task lands (e.g., one of the AZ-795 child tasks), the test-fixture inspection scope should include not just the new validator's tests but also the older happy-path tests of the same endpoint. A 30-minute "what test data does the prior happy-path test send, and does it satisfy the new validator?" check at spec-write time would have caught the `NextTestCoordinate` bug before the implementation phase began. **Add this to cycle 9's task-spec quality gate.**
### Pattern 3 — Perf-script contract sync is a partial-update class of bug (NEW pattern)
The cycle-8 batch-1 AZ-812 work updated `scripts/run-performance-tests.sh` PT-03/04/05/07 JSON bodies from `latitude/longitude` to `lat/lon`. The cycle-8 batch-3 AZ-809 work added new required fields (`requestMaps`, `createTilesZip`) to the route-creation contract. **The perf script's PT-06 body was NOT updated with the new required fields during batch 3.** The omission was caught at Step 15 (Performance Test gate) when PT-06 returned HTTP 400; a 1-line script fix landed alongside the perf report in commit `32bc5c1`.
**Why this matters**: cross-script wire-format sync is a recurring class of bug. Cycle 7 had the same family of issue when migrating `tile-inventory.md` 1.0.0 → 2.0.0 (`tileZoom/tileX/tileY → z/x/y`) — the perf script was updated, the integration tests were updated, but the cycle-7 deploy-report's example curl was a leftover sync miss that cycle 7's review caught manually. Cycle 8 has the same flavor (perf-script PT-06 partial sync).
**Insight**: there is no single owner for "every place that constructs the wire format outside of the production DTOs." The contract docs are the source of truth for the format, but the consumers (perf script, probe scripts, README example URLs, deploy-report example bodies, blackbox-tests.md trigger excerpts, OpenAPI examples) are scattered across the repo. **Recommendation**: add a `scripts/check_wire_format_consumers.sh` (cycle 9 candidate, ~2 SP) that ripgrep's the canonical contract-doc paths and reports any file path that has fewer wire-key matches than expected. This is a static-check level of defense — not a full schema verifier, just a "the field name is mentioned in N consumers, here are the N consumers, did you update them all?" surface.
### Pattern 4 — In-cycle security-finding resolution is a new tool in the autodev toolbox (NEW positive pattern)
Cycle 8 is the first cycle where a Step-14 security-audit Medium finding (F-AZ809-1) was resolved within the same autodev invocation: discovered in the audit (commit `ac40a8b`), fixed in commit `8fca6e0` (`MaxPolygons = 50` cap + unit test + integration test + `route-creation.md` v1.0.1 patch bump + traceability row AZ-809 AC-1b), all in ~30 minutes. The user-driven A/B/C gate at Step 14 explicitly enabled this: option A was "fix F-AZ809-1 NOW" rather than the historical default "defer to cycle N+1."
**Why this matters**: every prior cycle's security audit deferred all findings to a follow-up cycle (or the deploy-report's "Recommended follow-ups" section). The deferral pattern is correct for Low / informational findings and for any finding whose fix is non-trivial, but for a Medium finding whose fix is a one-line cap + ~5 lines of tests + a contract patch bump, the in-cycle resolution is meaningfully better: (a) the fix ships with the same commit chain that introduced the surface (one history rather than "introduced in cycle N, fixed in cycle N+1"), (b) the contract version reflects the fix immediately (route-creation.md v1.0.1 not v1.0.0), (c) the traceability matrix and blackbox-tests.md sub-cases are written at the time the finding is fresh, not from a stale finding description three weeks later.
**Insight**: the autodev orchestrator's Step-14 Optional Skill Gate should explicitly enumerate "fix-in-cycle" as a first-class A/B/C option for Medium findings whose fix is small (≤2 SP, ≤2 files, ≤1 contract bump). The current cycle-8 prompt did this ad-hoc; cycle 9's autodev flow should codify the threshold. **Recommendation**: amend `.cursor/skills/autodev/flows/existing-code.md` Step-14 action with a sub-clause: "If Step 14 surfaces a Medium finding whose remediation fits the small-fix threshold (≤2 SP, ≤2 files, ≤1 contract bump), present a 3-way gate: A) fix in-cycle then proceed | B) defer to cycle N+1 | C) skip Step 15+16+16.5 and end cycle. Default recommend A."
### Pattern 5 — Cycle 8 sustained the "zero new architecture-layer findings" streak across the project's largest delivery cycle (NEW positive pattern)
Cycle 8 delivered the most code volume in any single cycle of the project's history (5 tasks, 17 SP, 9 new validator files, 63 new unit tests, 52 new integration tests, 3 new contracts, 4 new probe scripts) AND held every structural metric stable:
- 0 new cross-project edges (3 consecutive cycles).
- 0 new csproj (5 consecutive cycles).
- 0 import-graph cycles (5 consecutive cycles).
- 0 new NuGet packages (3 consecutive cycles).
- 0 schema migrations (2 consecutive cycles).
- 0 new architecture-layer findings (after F-AZ809-1 resolution).
**Why this matters**: large-volume cycles historically correlate with new architecture deltas (cycle 1 added 4 csproj; cycle 4 added FluentValidation; cycle 5 added the Uuidv5 cross-cutting + 1 new import edge). Cycle 8 broke that correlation — the entire delivery stayed inside the established layer boundaries (`Api/Validators/` + `Common/DTO` annotations + `Tests/Validators/` + `IntegrationTests/*ValidationTests` + probe scripts + contract docs).
**Insight**: when a cycle's theme can be expressed as "add files in one well-defined directory + extend one existing API contract", the architecture-stability tax is near-zero even at high task volume. Cycles 9+ should explicitly favor task slates that share a single architectural surface (vs cross-cutting slates that touch multiple layers). **Recommendation**: when sizing a cycle's task slate, ask "are all tasks landing in ≤3 new files in the same namespace + ≤2 existing contract bumps?" — if yes, the cycle can safely deliver 4-5 tasks; if no, cap at 2-3 tasks.
## 6. Top 3 Improvement Actions
### Action 1 — Promote `FixedTimeProvider` and `PostBatch` test helpers to `SatelliteProvider.TestSupport`
**Why**: Pattern 1 from § 4 surfaced two DRY follow-ups during the cumulative review: `FixedTimeProvider` is duplicated across 4 test files (cycle-2 advisory crossed by cycle-8 batch 4 — "if a 3rd consumer appears, promote"); `PostBatch` is duplicated between `UavUploadTests.cs` and `UavUploadValidationTests.cs`. Cycle 8 explicitly recorded both as follow-up PBI candidates (`implementation_report_strict_validation_cycle8.md` § Follow-up PBIs).
**Action**: create AZ-NNN-promote-time-and-postbatch (2 SP — promote `FixedTimeProvider` to `SatelliteProvider.TestSupport.Time` + promote `PostBatch` to `SatelliteProvider.TestSupport.UavUploadMultipartFixture` + update 4 + 2 consuming test files). Sized as 2 SP because it's mechanical move + adjust imports + verify suite stays green. **Cycle 9 candidate, single batch.**
**Cost**: ~1 hour of dev work. Counted as 2 SP because it touches 6 files across 2 test projects.
### Action 2 — Add the strict-validation-fixture cross-check to task-spec quality gate
**Why**: Pattern 2 from § 4. The `NextTestCoordinate` latent-bug discovery cost the cycle-8 batch-4 implementation phase an extra ~30 minutes of investigation + a 2-file clamp fix. A 30-minute spec-write-time inspection ("does the prior happy-path test's data satisfy the new validator?") would have caught it before the implementation phase began. This is a process-level change to the task-spec quality gate, not a one-off fix.
**Action**: amend `.cursor/skills/new-task/SKILL.md` (or `.cursor/skills/plan/SKILL.md` if that's where strict-validation child specs are written) with a new spec-quality checklist item: "**Fixture pre-check**: for every existing test file that exercises the endpoint being newly validated, identify the test-data generator (helper, fixture, constant), trace one representative value end-to-end through the new validator rules, and explicitly confirm in the spec that the prior data satisfies the new rules — or document the required test-data update." ~1 SP.
**Cost**: ~30 minutes of skill-author work. Counted as 1 SP because it's a single checklist item added to one (or two) skill files.
### Action 3 — Build a `scripts/check_wire_format_consumers.sh` static-check probe
**Why**: Pattern 3 from § 4. Perf-script PT-06 partial sync was a known class of bug (cycle 7 had a similar instance with `tile-inventory.md` 1.0.0 → 2.0.0). There is no single owner for "every place that constructs the wire format outside the production DTOs," and the consumers are scattered (perf script, probe scripts, README example URLs, deploy-report example bodies, blackbox-tests.md trigger excerpts, OpenAPI examples). A static-check probe at the cycle-8 closure level would have caught the PT-06 omission before Step 15.
**Action**: create AZ-NNN-wire-format-consumer-check (2 SP — write `scripts/check_wire_format_consumers.sh` that ripgrep's the canonical contract-doc paths under `_docs/02_document/contracts/api/` and reports any consumer file path that mentions an old wire-key the contract has retired; integrate into the test-spec sync skill's quality gate). Initial implementation can be a simple "for each contract doc, extract the JSON field names from the contract's example body; for each consumer path, count matches; flag any consumer with fewer matches than expected after a known rename." ~2 SP.
**Cost**: ~1 hour of skill + script work. Counted as 2 SP because it requires (a) the bash script, (b) integration into the test-spec skill's quality gate, and (c) one validation run against cycle 8's actual surface to confirm the cycle-8 PT-06 miss would have been caught.
## 7. Carry-overs (status this cycle)
| Item | Status | Notes |
|------|--------|-------|
| `_docs/_process_leftovers/` folder state | **EMPTY** as of cycle 8 entry and exit | Third consecutive cycle with zero process leftovers. |
| Microsoft.NET.Test.Sdk 17.8.0 transitive `NuGet.Frameworks` NU1902 (D2-cy4, **Medium**) | OPEN (carried from cycles 4 + 5 + 6 + 7 + 8) | No cycle-8 touch. Re-flagged in `dependency_scan_cycle8.md` carry-over table. |
| Microsoft.IdentityModel.Tokens / System.IdentityModel.Tokens.Jwt 7.0.3 NU1902 (Low) | OPEN (carried from cycles 3 + 4 + 5 + 6 + 7 + 8) | No cycle-8 touch. Re-flagged. |
| Serilog.AspNetCore 8.0.3 → 10.x recheck (Low) | OPEN (carried from cycles 4 + 5 + 6 + 7 + 8) | No cycle-8 touch. Re-flagged. |
| ASPDEPR002 `WithOpenApi(...)` deprecation | OPEN (carried from cycles 4 + 5 + 6 + 7 + 8) | No cycle-8 touch. |
| Admin team `iss/aud` confirmation (carried from cycle 3) | OPEN (still required before promoting beyond `dev`) | Re-listed. |
| `metadata.flightId` authenticated provenance (F1-cy5) | OPEN (long-term, not actionable until flight registry exists) | Re-listed. |
| `pgcrypto` ops gap (F2-cy5) | OPEN (doc-only fix, ~30 min) | Re-listed. |
| Deployment runbook: ingress TLS termination + HTTP/2 forwarding (cy6 follow-up) | OPEN (no cycle-7 or cycle-8 touch) | Re-listed. |
| `tile-storage.md` consumer audit post v2.0.0 (cy6 follow-up) | OPEN | Re-listed. |
| Inventory endpoint `estimatedBytes` field (AZ-505 deferral) | OPEN | Re-listed. |
| HTTP/3 / QUIC dev listener (AZ-505 deferral) | OPEN | Re-listed. |
| Cycle-7 D-AZ795-1: FluentValidation 12.0.0 → 12.1.1 bump | OPEN (carried from cycle 7) | Cycle 8 explicitly excluded NuGet hygiene per scope discipline. |
| Cycle-7 F-AZ795-1 + F-AZ795-2: sanitize `JsonException.Message` + `BadHttpRequestException.Message` in 400 detail | OPEN (carried from cycle 7) | Cycle 8 added a third instance of this class (F-AZ810-1 in `UavUploadValidationFilter`) — see new cycle-8 entry below. |
| Cycle-7 implementation-report exit-gate / fallback formalisation | **CLOSED in-cycle by cycle 8** | Cycle 8 produced both per-batch reports AND a consolidated implementation report AND a completeness report AND a cumulative cross-batch review — four artifact types where cycle 7 had two. The implement skill self-corrected without a rule-file change. |
| Cycle-7 AZ-795 child-task sweep across remaining public endpoints | **CLOSED in-cycle by cycle 8** | Cycle 8 shipped all four AZ-795 child tasks (AZ-808 + AZ-809 + AZ-810 + AZ-811) + the prerequisite AZ-812 rename. See § 4 Pattern 1. |
| **NEW (cycle 8)** F-AZ809-1: unbounded `geofences.polygons` DoS | **CLOSED in-cycle** (commit `8fca6e0`) | First cycle-N security-audit Medium resolved within cycle N. |
| **NEW (cycle 8)** F-AZ810-1: `JsonException.Message` echo in `UavUploadValidationFilter` | OPEN — recommended | New instance of the cycle-7 F-AZ795-1 pattern in a second code path. Roll into the cycle-7 F-AZ795-1/2 sanitization PBI. |
| **NEW (cycle 8)** F-AZ810-2: `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` | OPEN — informational | Freshness window drifts in non-UTC dev environments. Zero impact in UTC-deployed prod. Cycle 9 candidate, low priority. |
| **NEW (cycle 8)** Promote `FixedTimeProvider` + `PostBatch` test helpers to `SatelliteProvider.TestSupport` | OPEN — recommended | Action 1 above. ~2 SP. |
| **NEW (cycle 8)** Strict-validation-fixture cross-check in task-spec quality gate | OPEN — recommended | Action 2 above. ~1 SP. |
| **NEW (cycle 8)** `scripts/check_wire_format_consumers.sh` static probe | OPEN — recommended | Action 3 above. ~2 SP. |
| **NEW (cycle 8)** PT-07 perf-harness cross-run cache pollution (hard-coded base coords) | OPEN — perf harness cleanup | Documented in `perf_2026-05-23_cycle8.md` § Known harness quirks. Pre-existing issue surfaced by cycle-8 back-to-back perf runs. ~2 SP cycle-9 candidate. |
| **NEW (cycle 8)** F-AZ809-1 adversarial perf scenario (PT-NN — 50 + 51 polygons → measure validator latency and 400-response time) | OPEN — perf harness extension | Converts F-AZ809-1 cap intent (DoS-bound) into a measurable regression gate. ~3 SP cycle-9 candidate. |
| **NEW (cycle 8)** Structure-snapshot retro hard-gate | OPEN — process | Cycles 6 + 7 didn't write `structure_*.md`; codify as a Step-17 hard requirement in retrospective Step 1 self-verification. ~1 SP. |
| **NEW (cycle 8)** Autodev Step-14 in-cycle-fix Optional Skill Gate codification | OPEN — process | § 4 Pattern 4 — amend `.cursor/skills/autodev/flows/existing-code.md` Step-14 to add "fix-in-cycle" as a first-class A/B/C option for Medium findings whose fix is small (≤2 SP, ≤2 files, ≤1 contract bump). ~1 SP. |
**New leftovers carried out of cycle 8**: **0** (process leftovers folder remains empty). All cycle-8 follow-ups are tracked as recommended PBIs in this retro § 6 + § 7, not as process leftovers.
## 8. Suggested Rule / Skill Updates
| Target file | Change | Rationale |
|-------------|--------|-----------|
| `.cursor/skills/retrospective/SKILL.md` Step 1 self-verification | Promote `Structural snapshot written` from soft check to hard requirement (BLOCKING gate) when `module-layout.md` exists. Add: "If the most recent `structure_*.md` is more than 1 cycle old, compute the snapshot vs the most recent one and fold the intermediate cycles' deltas into the new snapshot's notes." | Pattern: cycles 6 + 7 skipped the snapshot; cycle 8 caught up but had to compute the delta vs cycle 5 directly. The hard gate prevents the gap from compounding. |
| `.cursor/skills/autodev/flows/existing-code.md` Step 14 action | Add: "If Step 14 surfaces a Medium finding whose remediation fits the small-fix threshold (≤2 SP, ≤2 files, ≤1 contract bump), present a 3-way gate: A) fix in-cycle then proceed | B) defer to cycle N+1 | C) skip Step 15+16+16.5 and end cycle. Default recommend A." | § 4 Pattern 4. Codifies the cycle-8 F-AZ809-1 resolution pattern as a first-class option. |
| `.cursor/skills/new-task/SKILL.md` (or `.cursor/skills/plan/SKILL.md` if strict-validation child specs are written there) — spec quality checklist | Add a "Fixture pre-check" item: "For every existing test file that exercises the endpoint being newly validated, identify the test-data generator (helper, fixture, constant), trace one representative value end-to-end through the new validator rules, and explicitly confirm in the spec that the prior data satisfies the new rules — or document the required test-data update." | § 6 Action 2. Would have caught the cycle-8 `NextTestCoordinate` latent bug at spec-write time. |
| `.cursor/skills/test-spec/SKILL.md` cycle-update mode | Add a "wire-format consumer cross-check" step: after any contract `[JsonPropertyName]` rename or new required-field addition, ripgrep the consumer paths (`scripts/run-performance-tests.sh`, `scripts/probe_*.sh`, `README.md` example URLs, `_docs/04_deploy/*.md` example bodies, `_docs/02_document/tests/blackbox-tests.md` trigger excerpts) and verify they reference the new field names. | § 6 Action 3, simplified — instead of a separate script, fold the check into the existing test-spec sync skill that already runs on contract changes. |
| `.cursor/skills/security/SKILL.md` Phase 5 (Security Report) | Add a "Resolved In-Cycle" row category to the consolidated security-report template, distinct from "Carried Forward". Document the cycle-8 F-AZ809-1 resolution as the reference example. | The current security-report template implicitly assumes all findings carry forward; cycle 8 demonstrated that in-cycle resolution is a viable third state. Codifying it in the template makes future reports clearer about what "PASS_WITH_WARNINGS" actually contains. |
## 9. Validations and Sources
- **Cycle-8 implementation artifacts parsed**: 4 batch reports (`batch_01..04_cycle8_report.md`), 4 review files (referenced from each batch report), 1 cumulative review (`cumulative_review_batches_01-04_cycle8_report.md`), 1 consolidated implementation report (`implementation_report_strict_validation_cycle8.md`), 1 completeness report (`implementation_completeness_cycle8_report.md`), 5 task specs in `_docs/02_tasks/done/`, 9 dev commits on the cycle-8 chain (full subject + body parsed for each).
- **Cycle-7 retro compared explicitly**: see § 4 trend table + § 5 Pattern 1.
- **Cycle-5 structure snapshot compared**: see § 3 + `structure_2026-05-23_cycle8.md` (cycles 6 + 7 deltas folded into the cycle-8 row).
- **Cross-cycle dependency tracking**: AZ-808 + AZ-809 + AZ-810 + AZ-811 all carry `Relates AZ-795` (the AZ-795 epic parent) in Jira. AZ-812 carries `blocks AZ-808` (the OSM rename must land before the region-POST validator's contract publishes with `lat`/`lon`). The cycle-5/6/7 lessons about explicit dependency-link choice were applied.
- **No skill-level escalations encountered**: no `retry_count: 3` failures in any sub-skill; no FAIL verdicts in any per-batch review or cumulative review; no FAIL verdict in security audit (after in-cycle F-AZ809-1 resolution); no FAIL verdict in perf gate (after in-cycle perf-script fix). Two in-cycle process-driven fixes (F-AZ809-1 + perf-script PT-06) executed cleanly via user A/B/C gates.
## 10. Self-Verification
- [x] All cycle-8 implementation artifacts parsed (4 batch reports, 4 review files, 1 cumulative review, 1 implementation report, 1 completeness report, 5 task specs, 9 commit bodies, perf report, all 5 security audit reports, structure snapshot).
- [x] Comparison with cycle-7 retro performed (§ 4 trend table).
- [x] Top 3 improvement actions concrete and actionable (§ 6).
- [x] Suggested rule/skill updates specific and tied to a target file (§ 8).
- [x] New cycle-8 follow-up PBIs (Action 1 + 2 + 3 + F-AZ810-1/2 + perf-harness cleanups + structure-snapshot hard-gate + Step-14 in-cycle-fix codification) cross-referenced between § 6, § 7, and the security/perf reports.
- [x] LESSONS.md ring buffer to be appended with top 3 cycle-8 lessons (§ 5 Patterns 1, 2, 4 distilled to single-sentence form) — applied in next step.
- [x] Structural snapshot written (`structure_2026-05-23_cycle8.md`); cycle-6 + cycle-7 deltas folded; gap noted as cycle-9 follow-up (Suggested Rule Update 1).
@@ -0,0 +1,99 @@
# Structural Snapshot — 2026-05-23 (post-cycle 8, strict-validation sweep)
Cycle 8 delta against `structure_2026-05-12_cycle5.md` (the most recent snapshot — cycles 6 and 7 did not write snapshots; **process gap** carried forward as a cycle-8 follow-up). Source of truth: `_docs/02_document/module-layout.md` + on-disk `*.csproj` graph + `_docs/02_document/contracts/`.
## Projects
| Layer | csproj | Cycle 6+7+8 delta vs cycle-5 snapshot |
|-------|--------|---------------------------------------|
| 1 (Foundation) | `SatelliteProvider.Common` | **+5 DTO files modified** (cycle 8 only): `DTO/RequestRegionRequest.cs` (`Lat`/`Lon` rename + `[JsonRequired]`), `DTO/CreateRouteRequest.cs` + `DTO/RoutePoint.cs` + `DTO/GeofencePolygon.cs` + `DTO/GeoPoint.cs` (`[JsonRequired]`), `DTO/UavTileMetadata.cs` (`[JsonRequired]` + `Items` on the batch envelope). Plus cycle-6: AZ-505 inventory DTOs. Plus cycle-7: AZ-794 `tileZoom/tileX/tileY → z/x/y` rename. |
| 1 (Foundation) | `SatelliteProvider.DataAccess` | **+1 migration in cycle 6** (`015_*.sql` — AZ-505 leaflet covering index); **0 migrations in cycle 7 + cycle 8.** Cycle 8 has zero schema touch — first 2-cycle stretch without schema change since cycle 3-4 (cycle 7 was the first migration-free cycle). |
| 1 (Foundation, shared DTO) | `SatelliteProvider.Common` (DTO sub-folder) | **No new DTO files this cycle**; cycle 8 added `[JsonRequired]` annotations to 5 existing files. |
| 3 (Application) | `SatelliteProvider.Services.{TileDownloader, RegionProcessing, RouteManagement}` | unchanged (zero service-layer changes in cycle 8 — the entire cycle landed in `SatelliteProvider.Api/Validators/` + `Common/DTO`) |
| 4 (API / Entry) | `SatelliteProvider.Api` | **+9 validator files in cycle 8** (`Validators/CreateRouteRequestValidator.cs`, `Validators/GeofencePolygonValidator.cs`, `Validators/GetTileByLatLonQueryValidator.cs`, `Validators/RegionRequestValidator.cs`, `Validators/RejectUnknownQueryParamsEndpointFilter.cs`, `Validators/RoutePointValidator.cs`, `Validators/UavTileBatchMetadataPayloadValidator.cs`, `Validators/UavTileMetadataValidator.cs`, `Validators/UavUploadValidationFilter.cs`); **+1 DTO file** (`DTOs/GetTileByLatLonQuery.cs`, nullable record). Plus cycle-6 + cycle-7 adds: `Validators/ValidationEndpointFilter.cs`, `Validators/ValidationEndpointFilterExtensions.cs`, `Validators/InventoryRequestValidator.cs`, `Validators/GlobalValidatorConfig.cs` (cycle 7 foundations). `Program.cs` extended by every batch (`.WithValidation<T>()`, endpoint filters, `.Accepts<>` / `.Produces<>` / `.ProducesProblem(400)` chains, transient registrations). |
| 5 (Test-Support) | `SatelliteProvider.TestSupport` | unchanged |
| 6 (Tests) | `SatelliteProvider.Tests` | **+8 test files in cycle 8** under `Tests/Validators/` (RegionRequestValidatorTests + GetTileByLatLonQueryValidatorTests + RejectUnknownQueryParamsEndpointFilterTests + CreateRouteRequestValidatorTests + RoutePointValidatorTests + GeofencePolygonValidatorTests + UavTileBatchMetadataPayloadValidatorTests + UavTileMetadataValidatorTests); **+63 unit-test methods** total. |
| 6 (Tests) | `SatelliteProvider.IntegrationTests` | **+5 test files in cycle 8** (RegionFieldRenameTests + RegionRequestValidationTests + GetTileByLatLonValidationTests + CreateRouteValidationTests + UavUploadValidationTests); **+52 integration-test methods** total. `Program.cs` updated by every batch to wire the new entry points into both `RunSmokeSuite` and `RunFullSuite`. `ProblemDetailsAssertions.cs` extended with `AssertErrorsContainsMention` shared helper. |
**Project count**: **9** (unchanged from cycle 5 — cycle 8 adds files to existing projects, doesn't add a new csproj).
## Cross-Project Import Edges (compile-time `ProjectReference`)
| Edge | Count | Cycle-6+7+8 delta |
|------|-------|--------------------|
| Api → {Common, DataAccess, TileDownloader, RegionProcessing, RouteManagement} | 5 | unchanged |
| TileDownloader → {Common, DataAccess} | 2 | unchanged |
| DataAccess → {Common} | 1 | unchanged |
| RegionProcessing → {Common, DataAccess} | 2 | unchanged |
| RouteManagement → {Common, DataAccess} | 2 | unchanged |
| Tests → {Api, TileDownloader, RegionProcessing, RouteManagement, Common, DataAccess, TestSupport} | 7 | unchanged |
| IntegrationTests → {TestSupport, Common} | 2 | unchanged (the cycle-5 +1 edge has held) |
**Total ProjectReference edges**: **21** (cycle 5: 21). Net delta across cycles 6 + 7 + 8: **0** — three consecutive cycles with zero new compile-time edges. This is the longest stretch of edge stability in the project's history.
## Source-import sites — cycle 8 delta
| Importer | Imports from | Cycle 8 delta |
|----------|--------------|---------------|
| `SatelliteProvider.Api/Validators/{Create,Get,Region,Route,Uav}*Validator.cs` (9 new files) | `FluentValidation` (12.0.0, cycle-7 dep) + `SatelliteProvider.Common.DTO` | NEW (every cycle-8 validator imports from Common.DTO; no new third-party imports). |
| `SatelliteProvider.Api/Validators/RejectUnknownQueryParamsEndpointFilter.cs` (NEW) | `Microsoft.AspNetCore.Http` (framework) | NEW (reusable query-param allow-listing filter). |
| `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs` (NEW) | `FluentValidation` + `Microsoft.AspNetCore.Http` + `System.Text.Json` + `SatelliteProvider.Common.DTO` | NEW (bespoke `IEndpointFilter` for the multipart endpoint). |
| `SatelliteProvider.Tests/Validators/*` (8 new files) | `FluentValidation.TestHelper` + `SatelliteProvider.Common.DTO` + `Xunit` | NEW (each test file scoped to one validator). |
| `SatelliteProvider.IntegrationTests/*ValidationTests.cs` (5 new files) | existing `IntegrationTests` infrastructure (`ProblemDetailsAssertions`, `TestRunMode`, `JwtTokenFactory`) + `SatelliteProvider.Common.DTO` | NEW (each file scoped to one endpoint's validation surface). |
| All other source files | unchanged | — |
**~25 new source-level import lines** across the new files; **all internal or framework**. **Zero new third-party imports.** FluentValidation 12.0.0 was added by cycle 7 (it's the foundation that AZ-795 epic builds on); cycle 8 expands its consumer set within the Api project but doesn't introduce a new dependency line.
## Graph properties
- **Cycles in project import graph**: **0** (clean DAG — unchanged for 5 consecutive cycles).
- **Average ProjectReferences per component**: 21 / 9 = **~2.3** (unchanged).
- **Max in-degree**: Common (still highest — **7** incoming edges: Api, TileDownloader, DataAccess, RegionProcessing, RouteManagement, Tests, IntegrationTests). Unchanged.
- **Max out-degree**: Tests (**7** — unchanged).
- **TestSupport position**: leaf-of-test-subgraph; no production-layer importers (unchanged).
## NuGet dependency hygiene (cycle 8)
| Package | Cycle-7 version | Cycle-8 version | Status |
|---------|-----------------|-----------------|--------|
| FluentValidation | 12.0.0 (cycle 7 add) | unchanged | The cycle-7 retro Action 1 recommended a 12.0.0 → 12.1.1 bump (`D-AZ795-1`). Cycle 8 did NOT take the bump — the security audit re-confirmed Low / Hardening severity, and the cycle-8 scope (per-endpoint child tasks) explicitly excluded NuGet hygiene per `coderule.mdc` scope discipline. **Still OPEN, carried forward.** |
| All other NuGet packages across all 9 csproj files | unchanged | **unchanged** | **Zero NuGet bumps this cycle.** Three consecutive zero-bump cycles (cycle 6 closed AZ-505's only NuGet bump; cycle 7 added FluentValidation 12.0.0 + extensions; cycle 8 used FluentValidation entirely against cycle-7's foundation). |
| Carry-overs (still OPEN) | Cycle-3 D2-cy4 (`Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks` flag — **Medium**); cycle-4 D4 (`Microsoft.IdentityModel.Tokens` / `System.IdentityModel.Tokens.Jwt` 7.0.3 NU1902 — Low); `Serilog.AspNetCore` 8.0.3 fallback (Low) | unchanged | All three remain explicitly out of cycle-8 scope (scope discipline). Re-listed in `_docs/05_security/dependency_scan_cycle8.md` carry-over table. |
## Database schema surface (cycle 8 delta)
| Object | Change | Source |
|--------|--------|--------|
| (no schema changes this cycle) | — | — |
**Zero schema changes in cycle 8** — second consecutive migration-free cycle (cycle 7 was the first). Cycle 8's strict-validation work lives entirely above the persistence layer (deserializer + FluentValidation + endpoint filters); no field validation requires a column change because all rules are *application-level enforced* (the database still accepts any well-formed value the validator lets through).
## Architecture / contract surface (cycle 8 delta)
- **3 NEW contracts published**:
- `_docs/02_document/contracts/api/region-request.md` v1.0.0 (AZ-808 — published with post-AZ-812 `lat`/`lon` wire format directly per the coordination clause).
- `_docs/02_document/contracts/api/route-creation.md` v1.0.0 → **v1.0.1** (AZ-809 v1.0.0 + an in-cycle PATCH bump for the F-AZ809-1 50-polygon cap added during the Step-14 security-audit follow-up).
- `_docs/02_document/contracts/api/tile-latlon.md` v1.0.0 (AZ-811).
- **1 contract MINOR bump**: `uav-tile-upload.md` v1.1.0 → **v1.2.0** (AZ-810 — additive: new "Metadata validation" section).
- **No contract MAJOR bumps this cycle** (cycle 7 shipped the only MAJOR bump in project history — `tile-inventory.md` 1.0.0 → 2.0.0; cycle 8 produced only new contracts + minor + patch).
- **Public-API contract coverage**: now **5 contracts** across **5 documented endpoints** (`region-request`, `route-creation`, `tile-latlon`, `tile-inventory`, `uav-tile-upload`) + the shared `error-shape.md`. Endpoints without a dedicated contract: 2 read-only `GET /api/satellite/{region,route}/{id}` (path-Guid only — no strict-validation surface, per cycle-8 implementation report).
- **Contract-per-endpoint ratio**: **5 / 7** = **71%** (cycle 7: 2 / 7 = 29%, cycle 6: 1 / 7 = 14%). Cycle 8 more than doubled the documented-endpoint coverage in a single cycle.
- **Architecture findings carry-over from cycle 7**: 0 new architecture-layer findings; 0 architecture-layer carry-overs resolved (cycle 7's open recommendations on implement-skill ↔ downstream-skill artifact contract were partially addressed — cycle 8 produced both the per-batch reports AND the consolidated `implementation_report_strict_validation_cycle8.md` per cycle-7 Action 1 recommendation; see `retro_2026-05-23_cycle8.md` § Pattern 1).
## Net Architecture delta vs cycle 7
- **Resolved (closed by this cycle)**: **1** — cycle-7 Pattern 1 (missing implementation report) is closed by `implementation_report_strict_validation_cycle8.md` + `implementation_completeness_cycle8_report.md` + `cumulative_review_batches_01-04_cycle8_report.md` (three artifact types where cycle 7 had none).
- **Newly introduced**:
- 1 Medium **resolved in-cycle**: F-AZ809-1 (unbounded `geofences.polygons` DoS) — found in Step 14 security audit, fixed in commit `8fca6e0` with a `MaxPolygons = 50` cap + unit + integration test + contract v1.0.1 + traceability row AZ-809 AC-1b before Step 14 closure. **Net contribution: 0 (resolved before retrospective).**
- 2 Low (F-AZ810-1 `JsonException.Message` echo in `UavUploadValidationFilter`; F-AZ810-2 `DateTime` vs `DateTimeOffset` for `CapturedAt` — UTC-deployment-only impact). Open; carry to cycle 9.
- 0 new Medium, 0 new High, 0 new Critical (after F-AZ809-1 resolution).
- **0 cross-project edges added**, **0 import-graph cycles introduced**, **0 NuGet additions**, **0 migrations**.
- **Net Architecture delta**: **-1** (Pattern 1 closed; only Low informational findings introduced, both carried forward). **First negative net architecture delta since cycle 4.**
## What this snapshot says about cycle 8's shape
Cycle 8 is the project's **largest single-cycle code volume** to date (5 tasks shipped in 4 batches; 17 SP delivered vs cycle 7's 8 SP; ~115 new test methods; 9 new validator files; 3 new contracts + 1 MINOR + 1 PATCH bump). It is also the **lowest structural-impact cycle of that size**: zero new csproj, zero new NuGet, zero new cross-project edges, zero schema changes, zero new architecture violations. The entire cycle stays inside the `SatelliteProvider.Api/Validators/` namespace + `Common/DTO` annotations + `Tests/Validators/` test directory + 4 probe scripts + 4 new docs — a clean layered addition on top of the cycle-7 foundation (`UnmappedMemberHandling.Disallow`, `GlobalExceptionHandler`, `error-shape.md` v1.0.0, FluentValidation 12.0.0).
Cycle 8 also closes Pattern 1 from cycle 7: the implement skill produced both per-batch reports AND a consolidated implementation report AND a completeness report AND a cumulative cross-batch review — four artifact types where cycle 7 had two (per-batch reports + per-batch reviews only). This is the directly visible execution of cycle 7's Action 1 ("formalise the implement-skill ↔ downstream-skill artifact contract") — done implicitly by the implement skill itself this cycle, rather than via the rule-file change cycle 7 recommended.
The DAG remains acyclic with the same 9 projects; max-in-degree is still `Common` at 7; the project's architectural shape has held stable across cycles 6 + 7 + 8 despite the steady delivery cadence.
+8 -10
View File
@@ -37,11 +37,17 @@ If the enum's wire string happens to match a member name case-insensitively (e.g
## Ring buffer (last 15 entries — newest at top) ## Ring buffer (last 15 entries — newest at top)
- [2026-05-23] [process] Step-14 security-audit Medium findings whose remediation fits the small-fix threshold (≤2 SP, ≤2 files, ≤1 contract bump) should be resolved within the same autodev invocation rather than deferred to cycle N+1 — the fix lands with the same commit chain that introduced the surface, the contract version reflects the fix immediately, and the traceability matrix and blackbox-tests.md sub-cases are written while the finding is fresh; codify the option as a first-class A/B/C choice in `.cursor/skills/autodev/flows/existing-code.md` Step-14 action (cycle 8: F-AZ809-1 unbounded `geofences.polygons` DoS — discovered in commit `ac40a8b`, resolved in commit `8fca6e0` with `MaxPolygons = 50` cap + unit + integration test + `route-creation.md` v1.0.1 patch bump, ~30 minutes from finding to fix landed).
Source: _docs/06_metrics/retro_2026-05-23_cycle8.md
- [2026-05-23] [process] Retrospective recommendations ship end-to-end in the next cycle only when they (a) name concrete tracker tickets / files / endpoints in the action text, (b) are sized as a coherent cycle theme rather than scattered one-off fixes, and (c) the next cycle's planning phase pulls the recommendation directly into the task slate without re-deriving scope — phrase recommendations to satisfy all three or they become multi-cycle carry-overs (cycle 7 Action 3 named the 4 AZ-795 child endpoints + the SP sizing → cycle 8 shipped AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 as the coherent strict-validation theme, first directly-traceable cross-cycle improvement-action end-to-end in project history).
Source: _docs/06_metrics/retro_2026-05-23_cycle8.md
- [2026-05-23] [tooling] When a contract introduces a new required field or renames a wire key, the test-spec sync step must ripgrep all consumer paths (`scripts/run-performance-tests.sh`, `scripts/probe_*.sh`, `README.md` example URLs, `_docs/04_deploy/*.md` example bodies, `_docs/02_document/tests/blackbox-tests.md` trigger excerpts, OpenAPI examples) and verify they reference the new shape — otherwise partial syncs surface at Step 15 Performance Test gate or worse in production (cycle 8: AZ-812 lat/lon rename updated the perf script but AZ-809's newly-required `requestMaps` + `createTilesZip` fields did not propagate to PT-06; caught at Step 15 with a 1-line script fix in commit `32bc5c1` — a static-check probe at batch closure would have caught it 2 days earlier).
Source: _docs/06_metrics/retro_2026-05-23_cycle8.md
- [2026-05-23] [process] When verifying a "no-regression" AC for an input-validation change ("AZ-NNN does not break existing tests"), the only sound evidence is a green integration-test run — tracing fixture variables back to their generators in source is insufficient because helpers can produce values outside the new bounds and previously slipped through silently when no validator existed; document the standard as "verified by reading source" → unconfirmed, "verified by full test run" → confirmed, and gate the batch report's AC table on the latter before the implement skill closes the batch (cycle 8: AZ-810 batch_04 AC-9 claimed "no AZ-488 regression" based on tracing `latitude = coord.Latitude` in test source, but `NextTestCoordinate` seeded by `(Ticks/TicksPerSecond) % 1_000_000` produced lat far above 90° at runtime; the false-PASS only surfaced at autodev Step 11 when the integration test run returned HTTP 400 from the new validator on the AZ-488 happy path). - [2026-05-23] [process] When verifying a "no-regression" AC for an input-validation change ("AZ-NNN does not break existing tests"), the only sound evidence is a green integration-test run — tracing fixture variables back to their generators in source is insufficient because helpers can produce values outside the new bounds and previously slipped through silently when no validator existed; document the standard as "verified by reading source" → unconfirmed, "verified by full test run" → confirmed, and gate the batch report's AC table on the latter before the implement skill closes the batch (cycle 8: AZ-810 batch_04 AC-9 claimed "no AZ-488 regression" based on tracing `latitude = coord.Latitude` in test source, but `NextTestCoordinate` seeded by `(Ticks/TicksPerSecond) % 1_000_000` produced lat far above 90° at runtime; the false-PASS only surfaced at autodev Step 11 when the integration test run returned HTTP 400 from the new validator on the AZ-488 happy path).
Source: _docs/03_implementation/batch_04_cycle8_report.md (AC-9 row) Source: _docs/03_implementation/batch_04_cycle8_report.md (AC-9 row)
- [2026-05-22] [process] When the implement skill ships a cycle's batch commit without writing `_docs/03_implementation/implementation_report_*_cycle{N}.md`, downstream skills (test-spec cycle-update, document task mode, retrospective Step 1) must fall back to reading the cycle's task specs in `_docs/02_tasks/done/` plus the commit body via `git log --grep='[AZ-...]'` — codify the fallback in those skills' instructions instead of leaving it as per-cycle improvisation, because the implicit contract between Step 10 and Steps 11-17 broke silently this cycle and only succeeded because every downstream skill happened to be robust enough to substitute (cycle 7: AZ-794+AZ-795+AZ-796 shipped as commit `865dfdb` with no report artifact; doc-skill auto-walked the diff, test-spec read the task specs, retrospective wrote from the deploy + security + perf reports — all worked, but the contract was never formal). - [2026-05-22] [process] When the implement skill ships a cycle's batch commit without writing `_docs/03_implementation/implementation_report_*_cycle{N}.md`, downstream skills (test-spec cycle-update, document task mode, retrospective Step 1) must fall back to reading the cycle's task specs in `_docs/02_tasks/done/` plus the commit body via `git log --grep='[AZ-...]'` — codify the fallback in those skills' instructions instead of leaving it as per-cycle improvisation, because the implicit contract between Step 10 and Steps 11-17 broke silently this cycle and only succeeded because every downstream skill happened to be robust enough to substitute (cycle 7: AZ-794+AZ-795+AZ-796 shipped as commit `865dfdb` with no report artifact; doc-skill auto-walked the diff, test-spec read the task specs, retrospective wrote from the deploy + security + perf reports — all worked, but the contract was never formal). **Status**: closed by cycle 8 — the implement skill self-corrected and produced both per-batch reports AND a consolidated implementation report AND a completeness report AND a cumulative cross-batch review.
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-22] [testing] When a strict-validation layer ships (`JsonSerializerOptions.UnmappedMemberHandling.Disallow`, FluentValidation rules, explicit DTO `[JsonRequired]`), expect the project's own integration tests to surface latent bugs the prior lenient defaults had been masking — silent PascalCase fallback property names, out-of-range fixture coordinates, wrong-cased JSON keys; correct them in the same PR or the test suite goes red and the strict layer looks like a regression instead of the bug-finder it is (cycle 7: `IdempotentPostTests.RoutePoint` had been posting `{"Lat":...}` against a `[JsonPropertyName("lat")]` DTO for months; the new strict deserializer caught it and the 2-line payload fix landed alongside the strict layer). - [2026-05-22] [testing] When a strict-validation layer ships (`JsonSerializerOptions.UnmappedMemberHandling.Disallow`, FluentValidation rules, explicit DTO `[JsonRequired]`), expect the project's own integration tests to surface latent bugs the prior lenient defaults had been masking — silent PascalCase fallback property names, out-of-range fixture coordinates, wrong-cased JSON keys; correct them in the same PR or the test suite goes red and the strict layer looks like a regression instead of the bug-finder it is (cycle 7: `IdempotentPostTests.RoutePoint` had been posting `{"Lat":...}` against a `[JsonPropertyName("lat")]` DTO for months; the new strict deserializer caught it and the 2-line payload fix landed alongside the strict layer; **cycle 8 instance**: `UavUploadTests.NextTestCoordinate` produced lat > 90°, caught by AZ-810 validator, 2-file clamp fix in batch 4).
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-22] [architecture] Contract MAJOR bumps for projects with ≤2 known consumers should ship without a wire-format adapter — the cost of maintaining a dual-accepting endpoint outweighs the benefit when the operator runbook can coordinate the consumer cut-over directly; only invest in an adapter when consumer count or release-cadence asymmetry makes coordinated cut-over impractical (cycle 7: `tile-inventory.md` 1.0.0 → 2.0.0 renamed `tileZoom/tileX/tileY → z/x/y` and shipped breaking; the only consumer is `gps-denied-onboard` and the AZ-777 follow-up loop handled the coordination via the operator runbook in `deploy_cycle7.md`). - [2026-05-22] [architecture] Contract MAJOR bumps for projects with ≤2 known consumers should ship without a wire-format adapter — the cost of maintaining a dual-accepting endpoint outweighs the benefit when the operator runbook can coordinate the consumer cut-over directly; only invest in an adapter when consumer count or release-cadence asymmetry makes coordinated cut-over impractical (cycle 7: `tile-inventory.md` 1.0.0 → 2.0.0 renamed `tileZoom/tileX/tileY → z/x/y` and shipped breaking; the only consumer is `gps-denied-onboard` and the AZ-777 follow-up loop handled the coordination via the operator runbook in `deploy_cycle7.md`).
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
@@ -61,11 +67,3 @@ If the enum's wire string happens to match a member name case-insensitively (e.g
Source: _docs/06_metrics/retro_2026-05-12_cycle4.md Source: _docs/06_metrics/retro_2026-05-12_cycle4.md
- [2026-05-12] [process] When a scope-protected task newly *exposes* a pre-existing bug elsewhere in the codebase (vs. introducing a new one), surface it as a recommended follow-up PBI in the batch report AND list it as a "newly exposed bug" separate from "newly introduced findings" in the deploy report — bugs that already existed don't count as cycle-introduced regressions, but they must not be silently re-buried (cycle 4: AZ-500's bootstrap fix unmasked the pre-existing `scripts/run-performance-tests.sh:417` `grep -o | wc -l` + `pipefail` bug). - [2026-05-12] [process] When a scope-protected task newly *exposes* a pre-existing bug elsewhere in the codebase (vs. introducing a new one), surface it as a recommended follow-up PBI in the batch report AND list it as a "newly exposed bug" separate from "newly introduced findings" in the deploy report — bugs that already existed don't count as cycle-introduced regressions, but they must not be silently re-buried (cycle 4: AZ-500's bootstrap fix unmasked the pre-existing `scripts/run-performance-tests.sh:417` `grep -o | wc -l` + `pipefail` bug).
Source: _docs/06_metrics/retro_2026-05-12_cycle4.md Source: _docs/06_metrics/retro_2026-05-12_cycle4.md
- [2026-05-12] [process] When a cycle has a single non-functional task (migration / refactor / dependency hygiene), the retro must reframe the metric set around continuity (0 regressions), forward-resolution (prior findings closed by the bump itself), and unblocking (capabilities now exercisable end-to-end) — task count + complexity points read as misleading flatlines that look like under-productivity (cycle 4: AZ-500 alone delivered 5 SP vs cycle 3's 18 SP, but the cycle's value was forward-resolving 2 cycle-3 advisories and finally executing PT-01..PT-08 end-to-end against the migrated build).
Source: _docs/06_metrics/retro_2026-05-12_cycle4.md
- [2026-05-12] [process] For cross-team blockers (admin team must supply config values, etc.), prefer an Option-B forcing function (ship the validation/scaffolding with prod-empty config that fails-fast at deploy) over deferring the entire task — the fail-fast contract makes the cross-team conversation impossible to skip and ships the in-workspace work in the current cycle (cycle 3: AZ-494 shipped iss/aud validation with empty prod appsettings so deploy must supply real values).
Source: _docs/06_metrics/retro_2026-05-12_cycle3.md
- [2026-05-12] [process] ACs that prescribe a specific measurement or sentinel mechanism (e.g. "per-item latency < 50ms", "guard fires when DB name contains _test") should also prescribe — or explicitly defer — the path for collecting / enforcing it, or implementations will substitute proxies / equivalents that look like spec drift in review (cycle 3: AZ-492 PT-08 per-item gate cost became a derived proxy; AZ-493 DB-name guard became Host-allowlist).
Source: _docs/06_metrics/retro_2026-05-12_cycle3.md
- [2026-05-12] [process] ACs that require cross-repo writes should be tagged with the target workspace and rendered separately in the traceability matrix — mixing them with in-workspace ACs makes "correctly deferred" indistinguishable from "incomplete work" (cycle 3: AZ-494 AC-7 deferred for the suite-repo write; matrix renders as `◐ deferred` which is ambiguous).
Source: _docs/06_metrics/retro_2026-05-12_cycle3.md
+7 -7
View File
@@ -2,14 +2,14 @@
## Current Step ## Current Step
flow: existing-code flow: existing-code
step: 11 step: 17
name: Run Tests name: Retrospective
status: in_progress status: completed
sub_step: sub_step:
phase: 2 phase: 4
name: run-tests name: lessons-log-updated
detail: "re-run after coord-clamp fix" detail: "Cycle-end retrospective produced: _docs/06_metrics/retro_2026-05-23_cycle8.md + _docs/06_metrics/structure_2026-05-23_cycle8.md + _docs/LESSONS.md (3 new lessons, trimmed to 15 ring-buffer entries). Cycle 8 closed. Next /autodev invocation = cycle 9 Step 0 (orchestrator reset)."
retry_count: 1 retry_count: 0
cycle: 8 cycle: 8
tracker: jira tracker: jira
auto_push: true auto_push: true
+1 -1
View File
@@ -273,7 +273,7 @@ fi
echo "" echo ""
echo "PT-06: Route Point Interpolation Speed (threshold: 5000ms)" echo "PT-06: Route Point Interpolation Speed (threshold: 5000ms)"
ROUTE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') ROUTE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
BODY="{\"id\":\"$ROUTE_ID\",\"name\":\"Perf Test\",\"regionSizeMeters\":300,\"zoomLevel\":18,\"points\":[{\"lat\":48.276067,\"lon\":37.384458},{\"lat\":48.270740,\"lon\":37.374029}]}" BODY="{\"id\":\"$ROUTE_ID\",\"name\":\"Perf Test\",\"regionSizeMeters\":300,\"zoomLevel\":18,\"points\":[{\"lat\":48.276067,\"lon\":37.384458},{\"lat\":48.270740,\"lon\":37.374029}],\"requestMaps\":false,\"createTilesZip\":false}"
START=$(date +%s%N) START=$(date +%s%N)
HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$BODY" "$API_URL/api/satellite/route") HTTP_CODE=$(curl "${CURL_OPTS[@]}" -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$BODY" "$API_URL/api/satellite/route")