# Deploy Report — Cycle 7 (AZ-794 + AZ-795 + AZ-796)

**Date**: 2026-05-22
**Cycle**: 7
**Scope**: Three-task cycle delivering the **API quality follow-up** scope adopted from gps-denied-onboard's AZ-777 Phase 1 Jetson probe:
  - **AZ-794** — rename inventory body fields `tileZoom/tileX/tileY → z/x/y` (OSM / slippy-map convention).
  - **AZ-795** — epic + shared infra for strict input validation across all public endpoints: FluentValidation 12.0.0 wiring + global `ProblemDetails` exception handler + `JsonSerializerOptions.UnmappedMemberHandling.Disallow`.
  - **AZ-796** — first concrete per-endpoint child of AZ-795: strict validation for `POST /api/satellite/tiles/inventory` (9 validation rules); reference-implementation pattern for sibling per-endpoint tasks.

Cycle 7 is a **pure-quality cycle**: no new endpoints, no new persisted state, no migrations, no new env vars, no container-image changes. The full payload is a contract bump (`tile-inventory.md` 1.0.0 → 2.0.0 — major because of the field rename) plus a new strict validation surface across the inventory endpoint.

## What is shipping

### Code changes (committed to `dev`)

| Commit | Subject |
|--------|---------|
| `dceaddc` | `[AZ-794] [AZ-795] [AZ-796] Adopt cycle 7 tasks (API quality follow-up)` — Step 9 task-adoption commit, autodev state advanced to Step 10. |
| `865dfdb` | `[AZ-794] [AZ-795] [AZ-796] Strict input validation + z/x/y rename` — Step 10 implementation in a single batch. |
| _pending this commit_ | `[AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-16 sync (test-spec / docs / security / perf / deploy)` |

The two no-ticket commits `19c0371` (`[no-ticket] Sync .cursor with suite root`) and `7d3ba1c` (`Enhance .cursor documentation and workflows`) preceded cycle 7's task-adoption commit and are repo-plumbing changes (`.cursor/` skill + rule alignment with the suite root); they do not affect the running api and are not in cycle 7's tracker scope.

All commits are on `dev` but the cycle-7 sync commit (this one) has not yet been pushed to `origin/dev` as of this report. Operator runbook step 1 below covers the push.

### Database migrations

**None.** Cycle 7 ships **zero migration files**. The `_docs/02_document/contracts/data-access/tile-storage.md` v2.0.0 schema from cycle 6 is unchanged; the inventory endpoint reads from the same `tiles` table via the same `tiles_leaflet_path` covering index that cycle 6 introduced.

`pgcrypto`: still required, still installed automatically by migration 014 from cycle 5 — cycle 7 does not touch the extension surface.

### Configuration changes (operator must verify before promoting)

| Setting | Was | Now | Source |
|---------|-----|-----|--------|
| **No new env vars introduced.** | — | — | Cycle 7 carries forward the cycle-6 env contract verbatim (`JWT_SECRET ≥ 32B`, `JWT_ISSUER`, `JWT_AUDIENCE`, `GOOGLE_MAPS_API_KEY`). |
| `docker-compose.yml` Postgres host port | `5432:5432` | **`5433:5432`** (host-side bind only) | Dev-only sibling-project conflict avoidance (a sibling-suite Postgres was already binding `5432` on the dev workstation; moving the host-side bind to `5433` lets both projects run in parallel). **In-container port is unchanged (`5432`)** — the api service still resolves `postgres:5432` over the compose network. `appsettings.Development.json`, `README.md`, `AGENTS.md`, `architecture.md`, and `_docs/02_document/deployment/containerization.md` all aligned with the new host-side number. **Staging/prod unaffected** — they don't use docker-compose. |
| `appsettings.Development.json` Postgres connection | `Host=localhost;Port=5432` | **`Host=localhost;Port=5433`** | Aligns the .NET launch profile (i.e. `dotnet run` on host, NOT inside docker-compose) with the new host-side bind. Compose-internal connections (api ↔ postgres on the compose network) are unaffected. |
| Container image (`api` service) | `mcr.microsoft.com/dotnet/aspnet:10.0` | **unchanged** | No Dockerfile, no `.woodpecker/*.yml` changes this cycle. |
| Dev TLS dev-cert plumbing (cycle-6 addition) | TLS+ALPN with `./certs/api.{pfx,crt}`, `update-ca-certificates` in test container | **unchanged** | Cycle 7 reuses cycle-6's TLS plumbing verbatim. The new `scripts/probe_inventory_validation.sh` reuses `--insecure` for the dev cert and reads `JWT` from env. |
| `SatelliteProvider.Api.csproj` NuGet packages | (cycle-6 baseline) | **+ `FluentValidation` 12.0.0**, **+ `FluentValidation.DependencyInjectionExtensions` 12.0.0** | New dependencies for AZ-795's shared validation infra. Both packages have **no known CVEs** at 12.0.0 (NuGet audit clean, GitHub Security Advisories clean — `_docs/05_security/dependency_scan_cycle7.md` records the audit trace). Minor bump to 12.1.1 is the only recommended hardening (Low severity D-AZ795-1; bug fixes only, no security-driven advisory). |

### Contract changes (consumer-visible)

| Contract | Version | Change | Action for consumers |
|----------|---------|--------|----------------------|
| `POST /api/satellite/tiles/inventory` (`tile-inventory.md`) | **1.0.0 → 2.0.0** (MAJOR) | **Field rename**: request body and response payload renamed `tileZoom/tileX/tileY → z/x/y` (OSM / slippy-map convention; aligns the body shape with the existing URL convention on `GET /api/satellite/tiles/{z}/{x}/{y}`). **Strict validation**: HTTP 400 + RFC 7807 `ValidationProblemDetails` on any of: missing `tiles` / `locationHashes`, both arrays present (XOR violation), array empty, array exceeds 5000 entries, any `z` outside `[0, 22]`, any `x` outside `[0, 2^z)`, any `y` outside `[0, 2^z)`, any `locationHash` not 36-char lowercase UUID. **Unknown fields rejected**: any body containing a member not declared on the request DTO (e.g. legacy `tileZoom`, typo'd `Z`) is rejected via `JsonSerializerOptions.UnmappedMemberHandling.Disallow` at the deserializer layer (HTTP 400 before the validator runs). | **Sibling repo onboarding (gps-denied-onboard AZ-777 follow-up)**: any client carrying the legacy `tileZoom/tileX/tileY` body MUST switch to `z/x/y`. Any client expecting silent coercion of malformed bodies MUST handle the new 400 path. The `Authorization: Bearer …` header continues to be required (cycle-6 contract). Sibling-repo tasks for the per-endpoint sweep across the rest of the public API will follow as more AZ-795 children land. |
| `_docs/02_document/contracts/api/error-shape.md` | (existing baseline, AZ-353 sanitization) | **No version bump.** Cycle 7 confirms the error shape is RFC 7807-compatible (`type`/`title`/`status`/`detail`/`extensions.errors` for `ValidationProblemDetails`) and that 5xx errors continue to be sanitized via the cycle-6 baseline `GlobalExceptionHandler`. Two **Low** findings (F-AZ795-1, F-AZ795-2) note that `JsonException.Message` and `BadHttpRequestException.Message` may surface internal .NET type/parameter names in 400 detail strings — auth-gated, no security impact in dev — documented for sanitization in a future cycle. | Consumers parsing the 400 shape get a stable RFC 7807 envelope; no breaking change to the error contract itself. |
| `tile-storage.md` (data-access contract) | **unchanged at 2.0.0** | Cycle 7 does not touch the schema. The cycle-6 v2.0.0 contract from the AZ-503+AZ-505 joint freeze is preserved verbatim. | No action. |

### Container image

- **Source**: `SatelliteProvider.Api/Dockerfile` multi-stage build, base `mcr.microsoft.com/dotnet/aspnet:10.0` — **unchanged from cycle 5/6**.
- **No new mounts in `docker-compose.yml`**: the cycle-6 dev-cert mounts (`./certs/api.pfx:/app/certs/api.pfx:ro`) and the cycle-6 tests-container CA-trust mount remain unchanged.
- **Verification on dev workstation (local)**: `docker compose up -d --build` succeeded for the cycle-7 Step 15 perf run (this session). API healthy on `https://localhost:18980` (swagger 200; anonymous POST `/api/satellite/tiles/inventory` returns 401; v2 schema `{"tiles":[{"z":18,"x":...,"y":...}]}` returns 200; legacy schema `{"tiles":[{"tileZoom":18,...}]}` returns 400 — verified via `scripts/probe_inventory_validation.sh`).
- **Verification on CI**: pending — the cycle-7 sync commit (this one) has not been pushed yet. Operator action: after push, confirm the next Woodpecker `01-test` + `02-build-push` runs on `dev` succeed before promoting. Note that the cycle-7 .NET build uses `mcr.microsoft.com/dotnet/sdk:10.0` (unchanged) and the integration test container still resolves the dev cert via `scripts/run-tests.sh`'s `ensure_dev_cert` block; no new CI secret is required.
- **Multi-arch**: unchanged from cycle 6 (`aspnet:10.0` is multi-arch by Microsoft).

## Verification gates passed in this cycle

| Gate | Result | Evidence |
|------|--------|----------|
| Step 11 — Functional test suite | **PASS** | Unit suite 311 tests green (including the new 16-test `InventoryRequestValidatorTests` covering all 9 rules + the new `GlobalExceptionHandlerTests`); integration suite green (including the new `TileInventoryValidationTests` + the `TileInventoryTests` payload-rename refactor + the `IdempotentPostTests` adjacent fix where strict deserialization uncovered a long-silent PascalCase fallback bug). The implementation report for this cycle landed at the commit-message level — there is **no separate `_docs/03_implementation/implementation_report_*_cycle7.md` file**; cycle 7 was a single-batch cycle and the `865dfdb` commit body documents the implementation summary inline (the cycle-7 test-spec sync correctly notes this retrospectively as a process gap to address in cycle 8). |
| Step 12 — Test-Spec Sync | **PASS** | `_docs/02_document/tests/traceability-matrix.md` extended with 12 cycle-7 AC rows (AZ-794 ×3, AZ-795 ×3, AZ-796 ×6) + Coverage Summary update; `blackbox-tests.md` BT-27 added for the AZ-796 9-rule validation surface. |
| Step 13 — Update Docs | **PASS** | `_docs/02_document/architecture.md` already carried § 9 Input Validation (AZ-795) from the implementation commit; module-layout updated with cycle-7 file list; `tests_unit.md` documents the new `InventoryRequestValidatorTests` + `ValidatorTestModuleInitializer`; `tests_integration.md` documents `TileInventoryValidationTests` + `ProblemDetailsAssertions`; `glossary.md` gained entries for "Validation Problem Details", "FluentValidation", "Unmapped Member Handling"; `system-flows.md` F8 (Inventory Bulk Lookup) expanded with deserializer + validator gates and 13-row Validation Surface table; `data_parameters.md` § Tile Inventory documents the v2 input schema + constraints; `_docs/02_document/ripple_log_cycle7.md` captures the doc-side ripple decisions. |
| Step 14 — Security Audit | **PASS_WITH_WARNINGS** (3 Low findings) | `_docs/05_security/security_report_cycle7.md` (consolidated) + per-phase reports `dependency_scan_cycle7.md`, `static_analysis_cycle7.md`, `owasp_review_cycle7.md`, `infrastructure_review_cycle7.md`. Findings: **D-AZ795-1** (Low) FluentValidation 12.0.0 → 12.1.1 is a recommended bug-fix bump (no CVE driving it); **F-AZ795-1** (Low) `JsonException.Message` in the 400 detail string may leak the offending .NET type name on deserialization failure (auth-gated, dev-shown only); **F-AZ795-2** (Low) `BadHttpRequestException.Message` similarly may leak the parameter name on malformed-form-input cases (auth-gated). None are blocking; remediation is a sanitizer pass in a follow-up cycle. Architectural wins: mass-assignment prevention (`Disallow`), uniform 4xx contract (RFC 7807), auth-before-validation order confirmed in `Program.cs`. |
| Step 15 — Performance Test | **PASS** | `_docs/06_metrics/perf_2026-05-22_cycle7.md`. 8/8 scripted scenarios PASS (PT-01..PT-08), exit 0, single default-parameter run. Additionally, a cycle-7 PT-09 smoke probe (`/tmp/pt09_smoke.sh`, 20 sequential 2500-tile-batch calls using the new `z/x/y` schema, all-miss path) measured **min=27ms, median=44ms, p95=73ms, max=86ms** — **13.7× under** the AZ-505 AC-4 1000 ms p95 budget. The canonical PT-09 (`TileInventoryTests.PerformanceBudget_AC4`, all-hit seeded 2500 rows) remains the authoritative gate and is exercised by the integration suite. **AZ-794 / AZ-795 / AZ-796** added ≤ 10 ms of validator overhead on a 2500-item batch — well within noise band relative to the cycle-6 PT-09 number (p95=66ms). |

## Outstanding leftovers (status this cycle)

- **`_docs/_process_leftovers/`** is **empty as of cycle 7 entry** (cycle 6 closed the long-standing perf-harness leftover). Cycle 7 adds **zero new leftovers**.
- **Implementation-report process gap (NEW)**: cycle 7's Step 10 did not produce the expected `_docs/03_implementation/implementation_report_*_cycle7.md` artifact. The `test-spec` skill's `cycle-update` mode worked around it by reading the task specs + the `865dfdb` commit body as the implementation summary. Recommendation: surface as a Step-17 retro lesson; either tighten the implement-skill exit gate (require the report artifact before marking Step 10 complete) or update the test-spec / docs skills' resume protocol to formally consume the commit body when the report is absent.

## Recommended follow-up PBIs (out of cycle-7 scope, surfaced for backlog)

| ID | Estimate | Title | Why |
|----|----------|-------|-----|
| (TBD) | 1 SP | Bump `FluentValidation` 12.0.0 → 12.1.1 | D-AZ795-1 Low finding. Bug-fix-only release per the FluentValidation 12.x changelog (no CVE driving). Trivial package bump; pairs well with the unchanged `Microsoft.IdentityModel.Tokens` follow-up below. |
| (TBD) | 2 SP | Sanitize `JsonException.Message` + `BadHttpRequestException.Message` before surfacing in `ValidationProblemDetails.detail` | F-AZ795-1 + F-AZ795-2 Low findings. Replace the raw `Exception.Message` with a static string ("`Request body is not valid JSON`" / "`Form value could not be bound`") so the 400 path emits no internal .NET type / parameter names. Auth-gated, no security impact in dev — but the production contract should not leak this. |
| (TBD) | 2-3 SP per endpoint | Strict validation sweep for sibling public endpoints (`POST /api/satellite/request`, `POST /api/satellite/route`, `POST /api/satellite/upload`, `GET /api/satellite/tiles/latlon`, etc.) | AZ-795 epic continuation. AZ-796 is the **reference implementation**; the remaining child tasks reuse the same `InventoryRequestValidator` + `ValidationEndpointFilter` pattern. Estimate per endpoint depends on the number of validation rules and DTO complexity; expect 2-3 SP each. |
| (TBD) | 1 SP | Implementation-report exit gate for the `implement` skill | NEW process gap surfaced in cycle 7 — Step 10 completed without writing `_docs/03_implementation/implementation_report_*_cycle7.md`. The downstream skills (`test-spec` cycle-update, `document` task mode) compensate via task-spec + commit-body reading, but the report artifact is part of the autodev contract. Tighten the implement-skill exit gate to require the report file. |
| (TBD) | 3 SP (recheck per cycle) | Bump `Microsoft.IdentityModel.Tokens` / `System.IdentityModel.Tokens.Jwt` 7.0.3 → 7.1.2+ | Carry-over from cycles 3-6 (NU1902 moderate severity advisory). **Unchanged from cycle 6.** |
| (TBD) | 1 SP | Bump `Microsoft.NET.Test.Sdk` 17.8.0 → 17.13.0+ | Carry-over D2-cy4 (transitive `NuGet.Frameworks` flag). **Unchanged from cycles 4-6.** |
| (TBD) | 3 SP | Migrate `WithOpenApi(...)` callsites to ASP.NET Core 10 minimal-API metadata extensions | Carry-over from cycles 4-6 (`ASPDEPR002` warnings). **Unchanged from cycles 4-6.** |
| (TBD) | 1 SP (recheck per cycle) | `Serilog.AspNetCore` 8.0.3 → 10.x | Carry-over from cycles 4-6. **Unchanged from cycle 6 — no 10.x line published as of cycle 7 entry**; re-check at cycle-8 start. |
| (TBD) | 2 SP | Inventory endpoint `estimatedBytes` field | Deferred per AZ-505 Outcome bullet 1 — unchanged from cycle 6 carry-over. |
| (TBD) | 5 SP | HTTP/3 / QUIC dev listener | Deferred per AZ-505 Excluded list — unchanged from cycle 6 carry-over. |
| (TBD) | 1 SP | Deployment runbook: ingress TLS termination + HTTP/2 forwarding | Carry-over from cycle 6 — unchanged. |
| (TBD) | 1 SP | `tile-storage.md` consumer audit (post v2.0.0) | Carry-over from cycle 6 — unchanged. |

Admin team `iss/aud` confirmation (carried from cycle 3) remains OPEN as a long-standing ops-side gap; still required before promoting beyond `dev`. **Unchanged from cycle 6.**

## Operator runbook for promoting to staging / production

1. **Push** the cycle-7 sync commit + this deploy report to `origin/dev`. Confirm Woodpecker `01-test` runs green on `dev` (no new CI secret required; dev-cert plumbing is unchanged from cycle 6).
2. **No migration in this cycle.** The `tiles` schema is unchanged. `pgcrypto` already installed since cycle 5; no new extension dependency.
3. **Deploy** the new `dev-arm` (and amd64) image. The image base and Dockerfile are unchanged from cycle 6; the only build-output difference is the inclusion of the two new `FluentValidation` 12.0.0 assemblies. Container startup performance / cold-start latency is unaffected (FluentValidation registration is one-shot at DI build time).
4. **Smoke-test (production)** — note that cycle 7 introduces a **breaking** contract change on the inventory endpoint; the smoke must use the v2 schema:
   - `/swagger` (expect 200/301), `/api/satellite/region/<random>` (expect 401, JWT enforcement) — unchanged from cycle 6.
   - **v2 inventory body** (positive case): `POST /api/satellite/tiles/inventory` with a freshly-minted JWT, body `{"tiles":[{"z":18,"x":158485,"y":91707}]}` — expect **200** with one entry whose `present` field reflects whether that tile exists in the target environment.
   - **Legacy body** (negative case): same endpoint with body `{"tiles":[{"tileZoom":18,"tileX":158485,"tileY":91707}]}` — expect **400** with an RFC 7807 `ValidationProblemDetails` envelope. This confirms the new strict deserializer is active in the deployed image.
   - **Validator surface** (negative case): same endpoint with body `{"tiles":[{"z":99,"x":0,"y":0}]}` — expect **400** with the validator surface naming `z` as out-of-range.
   - Cycle-6 smoke (`POST /api/satellite/tiles/uav`) unchanged.
5. **Verify HTTP/2 negotiation against the production ingress** (one-off, not a regression test): unchanged from cycle 6 (`curl --http2 -sv https://<prod-host>/api/satellite/region/<id>` should log `* Using HTTP2` and a Bearer-rejected 401).
6. **No env-var change to coordinate.** Cycle 7 doesn't introduce any new app config. The dev-only Postgres host-port move (5432 → 5433) is `docker-compose.yml`-only and never reaches a non-dev environment.
7. **Consumer coordination**: notify all known consumers of the inventory endpoint of the v2 contract bump BEFORE the production deploy. Today's known consumers:
   - **gps-denied-onboard `TileDownloader`** (sibling repo): AZ-777 Phase 1 — the originating ticket already flagged the v2 schema; coordinate the cut-over flag flip with the onboard team. The onboard side's `c11.use_bulk_list_endpoint=true` flag (introduced in cycle 6) must also know which schema variant to emit; this is the onboard-side AZ-777 follow-up.
   - **Any direct curl / Postman clients** the ops team uses for smoke tests: the v1 body shape MUST be updated.
8. **Roll-forward plan**: if a regression appears post-deploy, the rollback target is the cycle-6 close `dev-arm` tag (built from `af66135`). The cycle-7 changes are pure-code (no migration, no schema change), so rolling back is safe and idempotent. Note that any consumer that already migrated to the v2 schema will receive an unexpected 200 from the rolled-back image with `tileZoom:0, tileX:0, tileY:0` echoed back (the cycle-6 silent-coercion bug). Coordinate any rollback with the same consumer set notified in step 7.
9. **Outstanding ops-side gap (long-standing, NOT new in cycle 7)**: admin team `iss/aud` confirmation before promoting beyond `dev`. Unchanged from cycles 3-6 runbooks.

## Differences vs. cycle 6 deploy

- **NEW**: a contract MAJOR bump (`tile-inventory.md` 1.0.0 → 2.0.0) — cycle 6 only **added** the inventory contract; cycle 7 is the **first revision** to it.
- **NEW**: strict input validation surface — FluentValidation 12.0.0 + global ProblemDetails handler + `UnmappedMemberHandling.Disallow`. Cycle 6 had no validation layer beyond model-binding's silent coercion.
- **NEW**: two NuGet additions (`FluentValidation` 12.0.0, `FluentValidation.DependencyInjectionExtensions` 12.0.0).
- **NEW**: a dev-only host-port move (Postgres `5432 → 5433`) for sibling-project conflict avoidance. Compose-internal traffic unchanged. Staging/prod unaffected.
- **NEW**: cycle-7 security audit ran (Step 14: PASS_WITH_WARNINGS with 3 Low findings) — cycle 6's Step 14 was skipped by the user.
- **NEW (process)**: cycle-7 Step 10 shipped without an explicit `implementation_report_*_cycle7.md` artifact; downstream skills compensated by reading the task specs + commit body. Recommended as a Step 17 retro lesson.
- **UNCHANGED**: container image base (`aspnet:10.0`), CI image (`sdk:10.0`), all env vars, all multi-arch tags, the cycle-4-and-earlier carry-over follow-up PBIs, the dev TLS cert plumbing, the cycle-6 Leaflet covering index (`tiles_leaflet_path`), `pgcrypto` extension state.
- **NO MIGRATION**: cycle 6 shipped migration `015_AddTilesLeafletPathIndex.sql`; cycle 7 ships none. The `tiles` schema is unchanged.
- **NO NEW ENDPOINTS**: cycle 6 added one new endpoint; cycle 7 modifies the contract of an existing endpoint without adding new routes.
- **NO HTTP/2 / TLS LAYER CHANGES**: cycle 6 introduced TLS+ALPN to the dev listener; cycle 7 leaves the listener untouched and reuses cycle-6's plumbing for the smoke / validation probe (`scripts/probe_inventory_validation.sh`).
