Third concrete child of AZ-795 (cycle 8 batch 3). FluentValidation +
[JsonRequired] + UnmappedMemberHandling.Disallow combine to reject every
malformed payload at the API boundary with RFC 7807 ValidationProblemDetails.
Validators (SatelliteProvider.Api/Validators/, all new)
- CreateRouteRequestValidator: id non-empty, name/description length,
regionSizeMeters/zoomLevel ranges, points count [2, 500], cross-field
createTilesZip => requestMaps. Chains RoutePointValidator (per-point)
and GeofencePolygonValidator (per-polygon, guarded by When(Geofences != null)).
OverridePropertyName("geofences.polygons") on the geofences chain so
FluentValidation's default leaf-only key policy doesn't drop the parent
path on deep expressions like req.Geofences!.Polygons.
- RoutePointValidator: lat/lon ranges; OverridePropertyName("lat"/"lon")
chained AFTER InclusiveBetween (the extension is defined on
IRuleBuilderOptions<T, TProperty>, so the generic type is only
inferable after the first concrete rule) so error keys match the
wire format (`points[i].lat`) rather than the C# property name
(`points[i].latitude`).
- GeofencePolygonValidator: per-corner range checks via private nested
GeoCornerValidator; cross-field NW.Lat > SE.Lat and NW.Lon < SE.Lon
invariants emit at errors["geofences.polygons[i].northWest"].
DTOs (SatelliteProvider.Common/DTO/, [JsonRequired] additions only)
- CreateRouteRequest: id, name, regionSizeMeters, zoomLevel, points,
requestMaps, createTilesZip
- RoutePoint: Latitude, Longitude
- GeofencePolygon: NorthWest, SouthEast; Geofences: Polygons
- GeoPoint: Lat, Lon
Tests
- Unit: 26 methods total — 16 in CreateRouteRequestValidatorTests, 6 in
GeofencePolygonValidatorTests, 4 in RoutePointValidatorTests. Each
RuleFor/RuleForEach chain has at least one positive + one negative case.
- Integration: CreateRouteValidationTests.cs — 16 methods (happy + 15
failure modes) wired into smoke + full suites. Covers empty body,
missing/zero id, empty name, out-of-range regionSizeMeters/zoomLevel,
points count < 2, per-point lat/lon out-of-range, geofence invariants,
missing requestMaps, cross-field createTilesZip, unknown root field,
nested type mismatch.
- Manual probe: scripts/probe_route_validation.sh curl-exercises every
failure mode end-to-end + happy path.
Docs
- New contract _docs/02_document/contracts/api/route-creation.md v1.0.0
with nested DTO chain, invariants, per-field test cases table, and
advisories on the legacy service-layer RouteValidator + the
input/output RoutePoint vs RoutePointDto naming asymmetry.
- system-flows.md F4 sequence diagram extended with the validation-filter
branch; preconditions + error scenarios reference the new contract.
- modules/api_program.md: CreateRoute handler section added; Api/Validators
bumped to AZ-808/AZ-809/AZ-811.
- modules/common_dtos.md: DTO descriptions updated with [JsonRequired]
annotations and constraint summaries.
- tests/blackbox-tests.md BT-06/BT-N03/BT-N04/BT-N05 align with the new
wire format and named error keys.
- tests/security-tests.md SEC-04 references GlobalExceptionHandler's
JsonException branch + AZ-353 correlationId.
- _docs/03_implementation/batch_03_cycle8_report.md + reviews/batch_03_cycle8_review.md
(PASS_WITH_NOTES — F1 Low: OverridePropertyName documented inline,
F2 + F3 Info: pre-existing advisories for follow-up).
Smoke green (mode=smoke, exit 0). AZ-809 transitioned to In Testing on Jira.
Task file moved to _docs/02_tasks/done/.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-808: FluentValidation for POST /api/satellite/request
- RegionRequestValidator: id non-empty, lat/lon/sizeMeters/zoomLevel ranges
- RequestRegionRequest: [JsonRequired] on every property, no implicit defaults
- Wired via .WithValidation<RequestRegionRequest>() in MapPost chain
- Unit + integration tests + curl probe script
- New contract: contracts/api/region-request.md v1.0.0
AZ-811: FluentValidation + envelope filter for GET /api/satellite/tiles/latlon
- GetTileByLatLonQuery: nullable record (double?/int?) so the minimal-API
binder never short-circuits with BadHttpRequestException before filters
- GetTileByLatLonQueryValidator: Cascade(Stop) + NotNull + InclusiveBetween
per param; missing surfaces as `\`<name>\` is required.`
- RejectUnknownQueryParamsEndpointFilter: reusable IEndpointFilter that
rejects any query key outside the allowed set with errors[<key>] map;
catches legacy `?Latitude=` typos and hostile probes (`?debug=1&admin=1`)
- Handler: [AsParameters] GetTileByLatLonQuery + .Value deref post-validator
- Unit (validator + filter) + integration tests + curl probe script
- New contract: contracts/api/tile-latlon.md v1.0.0
Shared hygiene
- Promote AssertErrorsContainsMention from per-test-file private helpers to
ProblemDetailsAssertions (closes batch-1 Low-severity DRY warning)
- Sync Swagger param descriptions, README, blackbox/security/perf scripts,
uuidv5 doc with the new lat/lon/zoom query-param names
Docs
- system-flows.md F1/F2 reference the new contracts + validation layers
- modules/api_program.md adds Api/Validators + Api/DTOs sections
- _autodev_state.md: batch 2 of 4 complete; next batch = AZ-809
All smoke tests green (mode=smoke, exit 0). AZ-808 + AZ-811 transitioned
to In Testing on Jira.
Co-authored-by: Cursor <cursoragent@cursor.com>
Marks transition from Step 9 (New Task, closed in 06d160d) to
Step 10 (Implement) for cycle 8. Batch 1 begins next (AZ-812
DTO rename, per the ordering decision recorded in the cycle-8
deps-table section).
Co-authored-by: Cursor <cursoragent@cursor.com>
Cycle 7 retrospective (cycle-end mode) — three-task pure-quality
cycle (AZ-794 rename + AZ-795 epic shared infra + AZ-796 inventory
validator). PASS gate end-to-end; first cycle to ship a contract
MAJOR bump; second consecutive cycle with zero new process
leftovers; first cycle to run the full 5-phase security audit
since cycle 5.
Top 3 improvement actions for cycle 8:
1. Formalise the implement-skill <-> downstream-skill artifact
contract — cycle 7 shipped without an implementation report
and the doc / test-spec / retrospective skills successfully
fell back to task-spec + commit-body reading, but the
fallback is implicit and should be codified.
2. Sanitize JsonException.Message + BadHttpRequestException.Message
before surfacing them in ValidationProblemDetails.detail —
F-AZ795-1 / F-AZ795-2 in the cycle-7 security audit.
3. AZ-795 child-task sweep across the remaining public endpoints
(request / route / upload / latlon) using AZ-796 as the
reference pattern; 2-3 SP per endpoint, spread across cycles
8-10.
LESSONS.md ring buffer updated with 3 cycle-7 entries (process /
testing / architecture); 3 oldest cycle-2 entries dropped to
maintain the 15-entry buffer.
State pointer advanced to cycle 8 step 9 (New Task) — Re-Entry
After Completion per autodev existing-code flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-794: rename inventory wire fields tileZoom/tileX/tileY -> z/x/y
to match the slippy-map URL convention. Contract bumped to v2.0.0.
AZ-795: shared validation infrastructure -- FluentValidation +
ValidationEndpointFilter + GlobalValidatorConfig (camelCase paths).
GlobalExceptionHandler now converts JsonException (UnmappedMember +
JsonRequired) into RFC 7807 ValidationProblemDetails. JSON layer
hardened with UnmappedMemberHandling.Disallow + camelCase naming
policy. New error-shape.md contract.
AZ-796: InventoryRequestValidator covers 9 rules (XOR tiles vs
locationHashes, cap 1000, z 0..22, x/y in slippy bounds, hash
length/charset). 16 unit tests + 16 integration tests + a manual
curl probe script.
Adjacent fixes uncovered by the new strict layer:
- IdempotentPostTests RoutePoint payload corrected to lat/lon
(the DTO has used JsonPropertyName for ages; previously silently
ignored under PascalCase fallback).
- TileInventoryTests slippy x/y reduced to fit z=18 bounds.
- docker-compose.yml host port for Postgres moved 5432 -> 5433 to
avoid sibling-project conflict; appsettings.Development + README
+ AGENTS + architecture + containerization docs aligned.
New coderule (suite + repo): API consumer-facing OpenAPI
descriptions must not contain task IDs, contract filenames, or
version-bump history -- internal change tracking belongs in
commits/contract docs/changelogs. Existing offending descriptions
in Program.cs cleaned up.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adopt three Jira tickets originally filed by gps-denied-onboard
AZ-777 Phase 1 Jetson probing into satellite-provider cycle 7.
- AZ-794: rename inventory body fields tileZoom/tileX/tileY → z/x/y
(OSM convention; aligns body shape with URL slippy-map convention).
- AZ-795: epic + shared infra ship for strict input validation across
all public endpoints (FluentValidation + global ProblemDetails
filter + JsonSerializerOptions.UnmappedMemberHandling.Disallow).
- AZ-796: first concrete per-endpoint child of AZ-795 — strict
validation for POST /api/satellite/tiles/inventory; reference
implementation pattern for sibling per-endpoint tasks.
Re-labels the cross-repo follow-up section in _dependencies_table.md
as Step 9 cycle 7, sets cycle 7 ordering (shared infra → rename →
inventory validator), and bumps the autodev state cursor to Step 10
(Implement) for cycle 7.
Source: gps-denied-onboard AZ-777 Phase 1 Jetson probe (2026-05-22).
Co-authored-by: Cursor <cursoragent@cursor.com>
Updated the README.md to reflect new skill commands and improved descriptions for various workflows, including the addition of new skills like /test-spec, /code-review, and /release. Enhanced clarity on session boundaries and flow resolutions in the auto-chaining process.
Removed the implementer agent file as it is no longer needed. Updated coderule and meta-rule documents to enforce stricter testing and implementation standards, ensuring real results are produced rather than simulated ones.
Revised the autodev flow documentation to include a new release step and clarified the retrospective process. Adjusted the testing rules to specify coverage thresholds for critical paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Single-task cycle delivering AZ-505 (3 SP); 1 batch, PASS verdict
after a single auto-fix round (ComputeLocationHash duplication
consolidated into Uuidv5.LocationHashForTile). Step 14 Security
Audit skipped; Step 15 Performance Test PASS (8/8, exit 0) and
closes the cycle-3 perf-harness leftover that carried across
cycles 3-5.
Top 3 lessons appended to LESSONS.md ring buffer:
- Kestrel Http1AndHttp2 requires TLS for ALPN; spec-time decision
- Dapper-bypassing test paths own the Npgsql type contract
- Test fixtures naming specific schema artifacts need migration awareness
Cycle 7 opens at Step 9 (New Task).
Co-authored-by: Cursor <cursoragent@cursor.com>
Step 15 (Performance Test): 8/8 PT scenarios PASS in a single
default-parameter run (exit 0). Adapts scripts/run-performance-tests.sh
for the new TLS+ALPN dev listener via CURL_OPTS=(--cacert ./certs/api.crt).
Report at _docs/06_metrics/perf_2026-05-12_cycle6.md. The clean exit-0
satisfies the cycle-3 perf-harness leftover deletion criterion that
carried across cycles 3-5; leftover file deleted.
Step 16 (Deploy): _docs/03_implementation/deploy_cycle6.md captures the
shipping payload (inventory endpoint, HTTP/2 TLS+ALPN, tiles_leaflet_path
covering index, migration 015), the dev-cert plumbing for local-docker +
integration-tests parity, the production-TLS topology note (terminate at
ingress; never promote the dev cert), and the operator runbook for
promoting cycle-6 past dev.
NU1902 / CA2227 / ASPDEPR002 / Serilog-10.x re-listed as carry-overs
unchanged; admin-team iss/aud confirmation unchanged.
State advanced to Step 17 (Retrospective).
Co-authored-by: Cursor <cursoragent@cursor.com>
Kestrel with HttpProtocols.Http1AndHttp2 on a plaintext listener
silently downgrades to HTTP/1.1-only (logs "HTTP/2 is not enabled
... TLS is not enabled"), so AC-5's multiplexed-GET test failed
with HTTP_1_1_REQUIRED. ALPN cannot run over plaintext, so the
fix switches the dev listener to TLS on https://+:8080:
- scripts/run-tests.sh generates a self-signed dev cert idempotently
(./certs/api.pfx + api.crt) via openssl in an alpine container;
certs/ is gitignored.
- docker-compose.yml binds Kestrel to ASPNETCORE_URLS=https://+:8080
with Kestrel__Certificates__Default__Path bound to the .pfx.
- docker-compose.tests.yml mounts api.crt into the integration-tests
container's CA store and runs update-ca-certificates so HttpClient
trusts the cert transparently; default API_URL is now https://api:8080.
- Drop the obsolete Http2UnencryptedSupport AppContext switch from
Http2MultiplexingTests; ALPN over TLS handles negotiation.
Test-data fixes caught on the post-TLS rerun (independent of the TLS
switch but surfaced together):
- Http2MultiplexingTests: switch slippy coords from (154321, 95812)
-- which Google Maps returns 404 for -- to (158485, 91707), the
slippy projection of (47.461747, 37.647063) already exercised by
JwtIntegrationTests.
- TileInventoryTests + LeafletPathIndexOnlyTests: SpecifyKind to
Unspecified at the binding site for raw Npgsql seed paths writing
into tiles.captured_at / created_at / updated_at (TIMESTAMP without
tz). Npgsql v6+ refuses Kind=Utc into plain timestamp columns;
production goes through Dapper and never hits this code path.
- MigrationTests Az503NewUniqueIndexCoversIntegerKeyAndFlightId:
accept either idx_tiles_location_hash (migration 014) or its
AZ-505 successor tiles_leaflet_path (migration 015) -- both have
location_hash as the leading column, which is the AC-9 intent.
Docs updated to reflect the TLS+ALPN path: tile-inventory.md
Non-Goals, modules/api_program.md, module-layout.md, the AZ-505
task spec's Risk 3, and the cycle 6 implementation + completeness
reports. The full integration test suite passes (mode=full, exit 0).
Co-authored-by: Cursor <cursoragent@cursor.com>
- Advanced the autodev state to Step 10, reflecting the implementation phase.
- Updated task name for AZ-505 to "Implement" and adjusted its status to "not_started."
- Enhanced the dependencies table with detailed context for AZ-505, clarifying its relationship with AZ-503 and its role in cycle 6.
Co-authored-by: Cursor <cursoragent@cursor.com>
- AZ-503 (3 SP, epic AZ-483) — Tile identity → UUIDv5 deterministic id;
integer-only UPSERT with COALESCE(flight_id) per-flight separation;
content_sha256 column; POST /api/satellite/tiles/inventory bulk-list
endpoint; HTTP/2 at Kestrel edge. Cross-workspace handoff from
gps-denied-onboard (AZ-304 / AZ-316 counterpart). Supersedes the
AZ-484 UPSERT-conflict-key portion.
- AZ-504 (1 SP, epic AZ-483) — Fix scripts/run-performance-tests.sh
lines 416-417: grep -o | wc -l + set -o pipefail kills PT-08 when
rejected=0. Closes the replay obligation for the cycle-3 perf-harness
leftover (leftover deletion gated on green full perf run, AC-4).
Updates _dependencies_table.md with cycle 5 entries and records
replay attempt #4 against the perf-cycle3 leftover (PBI opened —
leftover still stays until AZ-504 lands and full perf run is green).
State advanced to Step 10 (Implement).
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds retro_2026-05-12_cycle4.md, structure_2026-05-12_cycle4.md, and
the deploy_cycle4.md report that was dropped from the Steps 12-15
sync commit. Appends 3 new lessons to LESSONS.md (12/15 ring buffer)
on transitive major-version bumps, exposed pre-existing bugs, and
single-task-cycle metric framing. State advances to cycle 5 / step 9
(awaiting next New Task invocation).
Co-authored-by: Cursor <cursoragent@cursor.com>
Coordinated cross-cutting bump: 9 csproj TFMs net8.0 -> net10.0;
global.json sdk.version 8.0.0 -> 10.0.0; all Dockerfiles + scripts/
+ .woodpecker on mcr.microsoft.com/dotnet/{sdk,aspnet,runtime}:10.0;
all Microsoft.AspNetCore.* (8.0.25) and Microsoft.Extensions.* (9.0.10)
packages -> 10.0.7. Serilog.AspNetCore retained at 8.0.3 (10.0.0
requires Serilog.Sinks.File >= 7.0.0; out of AZ-500 scope per "no
unrelated package bumps") -- documented in AGENTS.md. Swashbuckle
9.x bumped to 10.1.7 to track Microsoft.OpenApi 2.x; Program.cs +
ParameterDescriptionFilter.cs refactored for the 2.x namespace
(Microsoft.OpenApi), OpenApiSecuritySchemeReference, JsonSchemaType
enum, and IOpenApiSchema dictionary properties. Fixed implicit AC-5
prereq: scripts/run-performance-tests.sh PERF_DLL path bin/Release/
net8.0 -> net10.0. Docs sync: architecture.md + AGENTS.md.
ACs verified: AC-1..AC-4 + AC-7 + AC-8 by grep + build; AC-6 by
./scripts/run-tests.sh --full (271/271 unit tests + full integration
suite green); AC-5 short bootstrap-smoke (PERF_REPEAT_COUNT=2
PERF_UAV_BATCH_SIZE=2) succeeded at the bootstrap step (no exit 3),
PT-01..PT-07 PASS. PT-08 surfaced a pre-existing grep-pipefail bug
in run-performance-tests.sh:417 -- not an SDK problem; recorded as
follow-up in the perf-cycle3 leftover. Code review verdict:
PASS_WITH_WARNINGS (2 Medium deferred per scope discipline:
WithOpenApi ASPDEPR002 deprecation x8, CS8604 nullable in
ParameterDescriptionFilter.cs; both targeted at follow-up PBIs).
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes Step 9 (New Task) of cycle 4. AZ-500 spec defines the
.NET 8 -> .NET 10 migration (TFM bump on 9 csprojs, global.json
SDK pin to 10.0.0, both Dockerfiles + run-tests.sh + woodpecker
to mcr.microsoft.com/dotnet/*:10.0, Microsoft.AspNetCore.* and
Microsoft.Extensions.* to the 10.x line, Serilog.AspNetCore to
10.x or documented 8.0.3 fallback, plus arch.md + AGENTS.md doc
sync). Closes the cycle-3 perf-harness leftover via AC-5
(bootstrap smoke after migration).
Also logs the cycle-4 perf-leftover replay attempt that
discovered the host-SDK / project-SDK mismatch and rolls the
state file from cycle 3 -> cycle 4 (Step 9 done -> Step 10
ready).
Co-authored-by: Cursor <cursoragent@cursor.com>
Cycle-3 retrospective:
- 6 tasks (AZ-491..AZ-496), 5 batches, 18 SP delivered.
- 100% code review pass rate (5/5 PASS_WITH_WARNINGS, 0 FAIL).
- 0 Critical/High/Medium review findings; 7 distinct Low.
- Security audit PASS_WITH_WARNINGS: 0 new Medium, 3 Low (all
test-only or operator-CLI), 2 Informational, 1 False Positive.
- Net Architecture delta: **-3** (F-AUTH-2 + D1 + D3 RESOLVED;
only new findings are Low test-side surfaces). First
net-negative cycle on record.
- 5 of 6 tasks completed first attempt (no post-review fix
commits). Cycle-2's 2 prior-retro actions all translated to
closed work (AZ-491 from Action 1, AZ-492 from Action 2,
AZ-493 from Action 3).
Top 3 cycle-4 improvement actions surfaced:
1. Execute the perf harness to capture PT-07/PT-08 baseline.
2. Bump TestSupport JWT pins 7.0.3 → 7.1.2+ (D4 NU1902 cleanup).
3. Add `workspace:` tag to cross-repo ACs in task-spec writing
and render them separately in the traceability matrix.
3 new ring-buffer lessons appended to _docs/LESSONS.md:
- [process] Option-B forcing functions for cross-team blockers.
- [process] ACs prescribing a measurement should also prescribe
the collection path.
- [process] Cross-repo-write ACs need workspace tags.
Structural snapshot at structure_2026-05-12_cycle3.md records the
new SatelliteProvider.TestSupport project (+2 ProjectReference edges
into it; no production-layer dependents) and the AZ-496 package
bumps (8.0.21 → 8.0.25).
Cycle 3 COMPLETE. State advanced to Step 9 (New Task) for cycle 4
per existing-code flow Re-Entry After Completion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Step 11 (Run Tests) is recorded as PASS based on the implement skill's
internal Step 16 gate (./scripts/run-tests.sh --full, all-green) per
test-run/SKILL.md § Functional Mode — same runner, immediately
preceding invocation, no value in a second run.
Step 12 (Test-Spec Sync, cycle-update mode):
- traceability-matrix.md: rows added for AZ-491 AC-1..AC-6,
AZ-493 AC-1..AC-6, AZ-495 (doc convention), AZ-496 AC-1..AC-N
(dependency bump); AZ-494 AC-1/AC-2 rows now cross-reference
new SEC-12 / SEC-13 blackbox IDs.
- security-tests.md: SEC-12 (wrong iss returns 401) and SEC-13
(wrong aud returns 401) appended for AZ-494.
- environment.md: Environment Variables table extended with
GOOGLE_MAPS_API_KEY, JWT_SECRET, JWT_ISSUER, JWT_AUDIENCE,
INTEGRATION_TEST_DB_RESET. Closes a cycle-2 oversight where
JWT_SECRET was never recorded.
Step 13 (Update Docs, task mode):
- tests_unit.md: consolidated the duplicate
AuthenticationServiceCollectionExtensionsTests entry that
spanned AZ-487 + AZ-494 into one coherent block.
- ripple_log_cycle3.md created: per-task source files +
every doc that was touched (architecture, module-layout,
api_program, tests_unit, tests_integration, traceability,
performance-tests, security-tests, environment, security_report,
owasp_review, deploy_cycle2, retro_2026-05-11_cycle2). Notes
which docs were intentionally NOT touched and the open
cross-repo doc ripple (AC-7).
Autodev state advanced to Step 13 completed. Next: Step 14 Security
Audit (optional gate).
Co-authored-by: Cursor <cursoragent@cursor.com>
Final cumulative review for batches 04-05 (PASS_WITH_WARNINGS, 4 Low
findings, all non-blocking). Combined with the prior 01-03 cumulative,
this closes the per-cycle batch coverage with two PASS_WITH_WARNINGS
verdicts.
scripts/run-tests.sh --full green: format check + 13 cycle-3 unit
tests (including the 4 new AZ-494 fail-fast cases for missing /
empty iss / aud) + the full integration suite (including the 2 new
WrongIssuer / WrongAudience 401 assertions).
Fixed a stale "leave blank to fall back" comment in .env.example
that contradicted the "REQUIRED" line right above it; the integration
runner reads env vars directly with no appsettings fallback so blank
values now fail-fast.
Advanced _docs/_autodev_state.md to mark Step 10 (Implement) status:
completed.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cycle-2 retrospective covering AZ-487 + AZ-488. Captures six patterns
(duplicate JWT helpers diverged then both broke; pre-existing
test bugs unmasked by downstream test pressure; cycle 1 perf-NFR
action stopped adding scenarios but did not drain backlog; doc-path
F1 carried over twice with no decision; integration test DB
isolation = wallclock workaround; 8 SP friction observable even
with user override). Top-3 improvement actions: consolidate JWT
mint helpers, promote PT-07/PT-08/JWT-attach to real PBI, real
integration DB-reset hook.
LESSONS.md ring buffer now holds 6 entries (testing x3, process x2,
estimation x1).
Structural snapshot: 6 components / 12 PR edges unchanged; contract
coverage 14% -> 29%; new external NuGet edges (JwtBearer 8.0.21 +
ImageSharp 3.1.11) tied to cycle-2 security findings.
Autodev pointer advances to cycle 3 / Step 9 New Task.
Co-authored-by: Cursor <cursoragent@cursor.com>
Step 15 (Performance Test) — skipped per gate (option B). Recording two
deferred items in the existing perf leftover:
* PT-07 + PT-08 remain Deferred. Both NFRs depend on the same
baseline-capture harness that has not landed; the integration-test
fixtures needed for PT-08 already exist (UavUploadTests +
UavTileImageFactory), so PT-08 attaches to the same harness as PT-07
when implemented.
* scripts/run-performance-tests.sh PT-01..PT-06 currently return 401
against the post-AZ-487 build because they attach no Bearer token.
Script must mint an HS256 token from JWT_SECRET at script start
before any curl call. Tracked in the leftover so PT-01..PT-06 are
runnable again the same cycle PT-07/PT-08 are activated.
No code change in this commit — leftover + state advance only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces the 501 stub at POST /api/satellite/upload with a multipart
batch endpoint that ingests UAV-captured tiles, runs each item through
a 5-rule quality gate, and persists accepted tiles via the AZ-484
multi-source storage path with source='uav'.
Quality gate (in fixed order, first failure wins): JPEG format
(content-type + magic), size band 5 KiB-5 MiB, exact 256x256
dimensions, captured-at age (no future >30 s skew, no older than
7 days), luminance variance on 32x32 downsample. Closed reject-reason
enumeration in v1.0.0 contract.
Authorization: custom PermissionsRequirement / PermissionsAuthorization
Handler that reads the JWT `permissions` claim (tolerates both
repeated-string and JSON-array shapes). Endpoint protected by
RequiresGpsPermission policy; 401 without token, 403 without GPS perm.
Persistence: file-first to ./tiles/uav/{z}/{x}/{y}.jpg, then
ITileRepository.InsertAsync UPSERT (per-source UPSERT contract from
AZ-484). Per-item failures reported in response without aborting the
batch. Kestrel MaxRequestBodySize and FormOptions limits set to
MaxBatchSize x MaxBytes (default 100 x 5 MiB = 500 MiB).
New frozen contract: _docs/02_document/contracts/api/uav-tile-upload.md
v1.0.0. PT-08 NFR added to performance-tests.md as Deferred (harness
work tracked in PT-07 leftover, per AZ-488 § Risk 4).
Tests: 11 quality-gate unit tests, 5 handler unit tests, 3 file-path
unit tests, 12 permission-handler unit tests, 7 integration tests
(AC-1..AC-6, AC-8). All 253 unit tests + smoke integration suite
green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds Microsoft.AspNetCore.Authentication.JwtBearer 8.0.21 and the
SatelliteProvider.Api.Authentication.AddSatelliteJwt extension that
validates HS256 tokens against a shared JWT_SECRET (>=32 bytes, fail
fast at startup). Every minimal-API endpoint now carries
.RequireAuthorization(); the middleware chain is UseExceptionHandler ->
UseHttpsRedirection -> UseCors -> UseAuthentication -> UseAuthorization
-> endpoints. Swagger UI gets a Bearer security definition so the
Authorize button works.
Test infrastructure: JwtTokenFactory (unit) and JwtTestHelpers
(integration) mint deterministic tokens against the same secret; the
integration test runner attaches a default Bearer token to its shared
HttpClient so existing tests continue to exercise protected endpoints.
JwtIntegrationTests adds AC-1..AC-4 and AC-7 (Swagger advertises
Bearer) end-to-end; AuthenticationServiceCollectionExtensionsTests
covers AC-5 (missing/empty/short secret fail-fast) plus env-var
precedence; JwtTokenFactoryTests covers AC-6 (claims pass through
the JwtSecurityTokenHandler.ValidateToken path JwtBearer uses).
docker-compose and scripts/run-tests.sh now propagate JWT_SECRET to
the api and integration-tests containers, with a >=32-byte guard.
.env.example documents the required keys; .env stays gitignored.
Code review verdict: PASS_WITH_WARNINGS (2 Low findings surfaced
in _docs/03_implementation/reviews/batch_01_cycle2_review.md).
Cross-component coordination: gps-denied-onboard and the mission
planner UI must attach Bearer tokens before this lands in dev.
Co-authored-by: Cursor <cursoragent@cursor.com>
Carries forward new-task research + solution drafts under
_docs/02_task_plans/uav-batch-upload/ that were not included in
the Step 9 task-spec commit (42a3cc7). Also marks the autodev
state as Step 10 in_progress for cycle 2 implementation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Created two PBIs for cycle 2 under epic AZ-483 (multi-source tile
storage + UAV upload). Splits the originally-planned single AZ-485
into 2 cohesive tasks because the combined scope was ~10 SP and
JWT auth is independently shippable:
- AZ-487 (2 SP) JWT validation baseline. Adds HS256 JwtBearer
middleware against JWT_SECRET env var per the suite-level auth
contract (suite/_docs/10_auth.md). Applies .RequireAuthorization()
on all existing endpoints. Skips iss/aud validation (suite doc
does not specify). No /users/me endpoints. Hard prerequisite for
AZ-488.
- AZ-488 (8 SP, over-cap user-accepted) UAV tile upload endpoint
with batch + 5-rule quality gate. Replaces the 501 stub. Multipart
batch DTO, 5 quality rules (format, size band, dimensions,
captured_at age 7d, blank/uniform variance heuristic). UAV files
land at ./tiles/uav/{z}/{x}/{y}.jpg; google_maps grandfathered
at bare ./tiles/{z}/{x}/{y}.jpg. Per-source UPSERT via the
AZ-484 ITileRepository.InsertAsync. Sync 200 with per-item
results. Requires GPS permission claim. Produces frozen contract
uav-tile-upload.md v1.0.0.
Both Jira tickets created and linked. Dependencies table updated.
Autodev state advanced to cycle 2 Step 10 (Implement).
Co-authored-by: Cursor <cursoragent@cursor.com>
Two integration-test failures uncovered after the initial commit:
1) GetTilesByRegionAsync outer ORDER BY referenced 'updated_at' but
the inner DISTINCT ON subquery aliased it to 'UpdatedAt' (Postgres
folds to 'updatedat'). DISTINCT ON already guarantees one row per
(latitude, longitude, ...) so the third tiebreak was unreachable;
removed it.
2) Dapper 2.1.35 silently bypasses SqlMapper.TypeHandler<T> for enum
types during read deserialization (Dapper issue #259). The
TileSourceTypeHandler worked for writes but reads fell through to
Enum.TryParse, which cannot map 'google_maps' to GoogleMaps.
Pivoted: TileEntity.Source is now a string (the wire value).
TileSource enum stays as the public producer surface in
Common.Enums; TileSourceConverter (Common.Enums) provides
ToWireValue / FromWireValue / IsValidWireValue at the boundary.
TileSourceTypeHandler deleted; registration removed from
DapperEnumTypeHandlers.RegisterAll.
tile-storage.md Inv-5 amended to document the storage choice.
_docs/LESSONS.md L-001 records the Dapper bypass for future cycles.
Full suite passes (213 unit + integration suite incl. AZ-484
AC-1..AC-5, security SEC-01..SEC-04, AZ-356/362/357).
Co-authored-by: Cursor <cursoragent@cursor.com>
Add per-source tile rows to support multi-provider imagery (Google
Maps + future UAV). Migration 013 (transactional) introduces
source/captured_at columns, backfills existing rows to
(source='google_maps', captured_at=created_at), and replaces the
4-column unique index with a 5-column index that includes source.
TileRepository:
- ColumnList includes source + captured_at
- GetByTileCoordinatesAsync returns most-recent row across sources
(ORDER BY captured_at DESC, updated_at DESC, id DESC)
- GetTilesByRegionAsync uses DISTINCT ON to pick the most-recent
tile per cell, restoring caller-facing row order
- Insert/Update upsert on the new 5-column conflict key
TileSource enum lives in Common.Enums. Snake_case wire format
(google_maps, uav) is enforced by a focused TileSourceTypeHandler
because the generic ToLowerInvariant pattern would emit
"googlemaps", violating contract v1.0.0.
TileService stamps Source=GoogleMaps + CapturedAt=UtcNow on every
new tile. Tile-storage contract is now frozen at v1.0.0.
AC coverage 7/7. New unit + integration tests cover all ACs;
existing 200 unit + 5 smoke tests preserved.
Co-authored-by: Cursor <cursoragent@cursor.com>
Step-9 (new-task) cycle 1 artifacts for the AZ-483 multi-source tile
storage epic. AZ-485 (UAV upload + quality gate) deferred to a future
Step-9 loop and recorded as planned in the dependencies table.
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 6 (Verification): smoke run green (format gate + 200/200
unit + integration smoke). verification_report.md captures
metric deltas vs Phase 0 baseline; all 5 ACs met, all 4
constraints honored, 0 regressions.
Phase 7 (Documentation):
- module-layout.md: corrected DataAccess->Common dependency
(was mistakenly documented as "Imports from: (none)" by
prior AZ-315 baseline; csproj reference + 7 import sites
have actually been there since AZ-309).
- architecture_compliance_baseline.md: F5 entry revised to
reflect the actual layering invariant (one-way: Common
MUST NOT import from DataAccess, but DataAccess MAY
import from Common).
- 00_discovery.md: added "Updates Since Baseline" section
enumerating the AZ-309 split + AZ-350 27-change run +
AZ-372 tooling additions; original tree kept as a
2026-05-10 snapshot.
FINAL_report: complete run summary (10 batches, 27 tasks,
3 K=3 cumulative reviews, baseline->final metric table,
remaining items, lessons learned).
Autodev state: advance Step 8 -> Step 9 (New Task);
sub_step reset to phase 0 awaiting-invocation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Batch 24 of 03-code-quality-refactoring run; closes the run.
AZ-375 (C22): GoogleMapsDownloaderV2.DownloadTilesGridAsync now
builds a HashSet<(int X, int Y, int Z)> once from existingTiles
and tests Contains((x, y, zoomLevel)) per cell. Removes the per-cell
FirstOrDefault tolerance scan and the unused _processingConfig
.LatLonTolerance reference at this site.
AZ-377 (C24): promote Earth + tile-pixel constants to a single
home. GeoUtils now exposes EarthRadiusMeters, EarthEquatorial
CircumferenceMeters, MetersPerDegreeLatitude as public const.
MapConfig adds DefaultTileSizePixels (const) wired as the
TileSizePixels property default. TileRepository and Google
MapsDownloaderV2 read those constants instead of duplicating
the literals 6378137, 40075016.686, 111000.0, and 256.
Tests: +6 new (DownloaderRefactorTests, extended GeoUtils
RefactorTests). 200/200 unit tests pass.
Cumulative K=3 review (batches 22-24): PASS_WITH_WARNINGS,
4 Low findings only — see
_docs/03_implementation/reviews/cumulative_review_22-24.md.
Tooling fix: scripts/run-tests.sh --unit-only path now restores
before testing (was failing on SixLabors resolution in clean
container). Stripped stray BOM from MapConfig.cs to satisfy the
.editorconfig charset gate.
Updates _dependencies_table.md to reflect all 27 03-code-quality-
refactoring tasks done; updates _autodev_state.md to refactor
phase 5 (test-sync).
Co-authored-by: Cursor <cursoragent@cursor.com>
Batch 23 of refactor 03-code-quality-refactoring (4 tasks, 5 SP):
- AZ-376 (C23): Delete unused FindExistingTileAsync from
ITileRepository / TileRepository. No callers; method also took the
obsolete `version` arg removed by C06/AZ-357.
- AZ-378 (C25): Repository _logger discipline.
TileRepository.GetTilesByRegionAsync now emits LogWarning when the
query exceeds SlowQueryThresholdMs (500 ms). RegionRepository and
RouteRepository drop the unused ILogger<TRepo> field, parameter, and
using; Program.cs DI registrations updated.
- AZ-379 (C26): Extract `private const string ColumnList` per repo
(TileRepository, RegionRepository, RouteRepository); SELECTs use
$@"SELECT {ColumnList} FROM ..." (C# 10+ const interpolation).
INSERT/UPDATE/DELETE unchanged; route_points single-site SELECT left
inline.
- AZ-380 (C27): Delete dead alias GeoUtils.CalculatePolygonDiagonalDistance.
Tests: +9 new (RepositoryRefactorTests x8, GeoUtilsRefactorTests x1)
covering each AC via reflection / file-content assertions; pattern
mirrors ToolingConfigurationTests (b22) and AcceptanceCriteriaRT2Tests
(b19). Unit suite 181 -> 190, all green. dotnet format clean.
Code review: PASS_WITH_WARNINGS (3 Low findings, all informational or
out-of-scope for this batch). See
_docs/03_implementation/reviews/batch_23_review.md.
Cumulative review counter 2/3; next K=3 review fires after batch 24.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pure whitespace-only cleanup uncovered by the new format gate from the
previous commit. Verified via `git diff -w --stat`: only 4 files differ
when whitespace is ignored, and those differ only by the BOM byte.
Cleanup kinds applied across 22 source files:
- BOM removal (MapConfig.cs, SatTile.cs, GeoUtils.cs,
IntegrationTests/Program.cs)
- CRLF -> LF (IntegrationTests/Program.cs)
- Trailing whitespace on blank lines (Common, Api, DataAccess,
IntegrationTests, Services.RegionProcessing,
Services.TileDownloader)
- Final newline added (RoutePoint.cs, GeoPoint.cs, others)
After this commit `dotnet format whitespace SatelliteProvider.sln
--verify-no-changes` exits 0; AC-1 is enforceable from `scripts/
run-tests.sh` going forward.
Also lands the batch 22 report, code-review report
(PASS_WITH_WARNINGS, 2 Low findings — both deferred per spec),
dependency-table status update (AZ-372 -> Done (In Testing)), task
archive (todo/ -> done/), and autodev state update.
Co-authored-by: Cursor <cursoragent@cursor.com>