Promotes 8 operational levers into config keys with defaults that match
the prior source literals byte-for-byte:
ProcessingConfig: RegionProcessingTimeoutSeconds (300),
RouteProcessingPollIntervalSeconds (5),
MaxRoutePointSpacingMeters (200), LatLonTolerance (0.0001).
MapConfig: TileSizePixels (256), AllowedZoomLevels ([15..19]),
RetryBaseDelaySeconds (1), RetryMaxDelaySeconds (30).
Sites updated: RegionService, RouteProcessingService,
RoutePointGraphBuilder, RouteValidator, RouteService 4-arg ctor,
RouteImageRenderer, GoogleMapsDownloaderV2, TileService. Closes LF-2 by
forwarding HttpContext.RequestAborted from GetTileByLatLon into the
downloader. appsettings.json gains the 8 new keys at default values.
Tests: 141 / 141 unit + 5 / 5 smoke green. New ConfigDefaultsTests pins
defaults to original literals; new TileService unit test asserts CT
identity from caller to downloader (AZ-371 AC-3).
Co-authored-by: Cursor <cursoragent@cursor.com>
Extracts RouteRegionMatcher, RouteCsvWriter, RouteSummaryWriter,
RouteImageRenderer, TilesZipBuilder, RegionFileCleaner from the
~750-LOC RouteProcessingService god-class. Moves TileInfo to its
own file as a sealed record. Replaces IServiceProvider scope-
locator with a direct IRegionService injection (folds AZ-360 / C08).
Updates DI registration and tests.
Tests: 133 / 133 unit + 5 / 5 smoke green; integration suite exit 0.
Pixel-equivalent stitched route image and byte-equivalent CSV /
summary / ZIP outputs verified through the smoke run.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract RouteValidator (aggregating validator), RoutePointGraphBuilder
(point interpolation + sequence numbering), GeofenceGridCalculator
(NW/SE region centers), and RouteResponseMapper (entity -> DTO; also
used by GetRouteAsync, eliminating duplicate DTO assembly).
CreateRouteAsync shrinks 184 -> 52 LOC of orchestration. RouteService.cs
shrinks 295 -> 138 LOC overall. Validation aggregates all failures into
a single ArgumentException (AC-2); single-violation messages preserved
verbatim so existing RouteServiceTests pass unchanged. 28 new unit
tests for the four helpers (112/112 unit tests, smoke green).
Co-authored-by: Cursor <cursoragent@cursor.com>
Move 5 cross-component DTOs (GetSatelliteTilesResponse, SatelliteTile,
SaveResult, DownloadTileResponse, RequestRegionRequest) to
SatelliteProvider.Common/DTO/. Keep UploadImageRequest in the API
project under SatelliteProvider.Api.DTOs (IFormFile depends on
Microsoft.AspNetCore.Http; pulling it into Common would force an
ASP.NET framework reference into the foundation layer and break the
module-layout "Common: Imports from: (none)" invariant). Move
ParameterDescriptionFilter to SatelliteProvider.Api.Swagger.
Program.cs shrinks from 366 to 257 lines and now contains only
endpoint wiring (AC-1). JSON wire shape and Swagger schema names are
preserved (AC-2). 84 unit + full integration suite green (AC-3).
Co-authored-by: Cursor <cursoragent@cursor.com>
Both RegionService.GenerateCsvFileAsync and
RouteProcessingService.GenerateRouteCsvAsync wrote the same CSV
shape: header "latitude,longitude,file_path", same
OrderByDescending(Latitude).ThenBy(Longitude) ordering, same F6
numeric format. Two near-identical writers with no shared abstraction.
Extracted TileCsvWriter (instance class, no DI dependencies) plus a
TileCsvRow record bridging the per-pipeline DTOs (TileMetadata vs
TileInfo) to a single contract. The header constant, ordering rule,
and StreamWriter lifecycle now live in one place.
Both call sites collapse to a one-line projection plus a delegated
WriteAsync call. Region method becomes static (no longer references
instance state). Route method preserves its existing logger line.
Coverage:
- 7 new unit tests including a byte-for-byte equivalence test that
writes the same input via both the new TileCsvWriter and the
inlined-original code path side by side and asserts file bytes
are identical.
- Integration smoke + full suite green; route + region CSV outputs
unchanged across all existing scenarios (verified by extended-route
CSV verification step in the integration suite).
- 84/84 unit tests pass (was 77).
Side improvement: writer now respects CancellationToken mid-loop.
The pre-refactor inline code did not. Strict improvement; consistent
with every other async API in the codebase.
Co-authored-by: Cursor <cursoragent@cursor.com>
RouteProcessingService.CalculateDistance(double, double, double, double)
re-implemented Haversine using EARTH_RADIUS=6371000 alongside the
canonical GeoUtils.CalculateDistance(GeoPoint, GeoPoint) which uses
EARTH_RADIUS=6378137. Two implementations of the same formula for the
same problem.
Separately, ExtractTileCoordinatesFromFilename in RouteProcessingService
parsed the tile_{z}_{x}_{y}_{ts}.jpg filename pattern that's *generated*
by StorageConfig.GetTileFilePath in another assembly — the writer and
parser were coupled by string convention only and a format change in
one would silently break the other.
Both fixes:
(a) Deleted the duplicate Haversine in RouteProcessingService. The
single call site (region-matching nearest-neighbor OrderBy) now uses
GeoUtils.CalculateDistance with GeoPoint instances. The constant
difference is monotonic-equivalent for OrderBy purposes — same region
is picked.
(b) Added static StorageConfig.TryExtractTileCoordinates(string, out
int, out int): bool — pure parser, co-located with GetTileFilePath so
the inverse-pair invariant is structural, not by-convention.
RouteProcessingService.ExtractTileCoordinatesFromFilename becomes a
thin wrapper that delegates to the helper and emits the existing
warning log on malformed input — the AZ-352 tests for warning behavior
all still pass.
Verification:
- 77/77 unit tests green (was 71 → +6 new StorageConfigTests including
a writer/parser round-trip test for AC-2).
- Smoke + full integration suite green.
- AC-1 grep verification: Math.Sin/EARTH_RADIUS patterns are now
confined to GeoUtils.cs only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Both POST /api/satellite/request and POST /api/satellite/route accept
a caller-supplied id (Guid). Before this change, a retried POST with
the same id would either crash with a unique-key violation (regions)
or quietly create a divergent row (routes), neither of which matched
the documented intent of caller-supplied GUIDs.
RegionService.RequestRegionAsync and RouteService.CreateRouteAsync
now check for an existing row by id at the top of the method. If one
is found, the existing resource is returned with HTTP 200 and the
side effects (insert + enqueue + point regeneration + geofence-region
queueing) are all skipped. The Information-level log line on the
idempotent path makes retries observable.
OpenAPI Description metadata documents the contract on both endpoints
so client integrators see it in Swagger.
Coverage:
- 2 new unit tests (one per service) assert that on duplicate id no
insert / enqueue / point-generation / region-queueing call is made.
- 2 new integration tests (IdempotentPostTests.cs) exercise the
contract end-to-end via HTTP, asserting both calls return 200 and
CreatedAt matches within 1ms (PostgreSQL truncates TIMESTAMP to
microseconds while .NET DateTime keeps 100ns ticks; a real
re-insertion would shift CreatedAt by milliseconds at minimum).
Note: the check-first pattern leaves a TOCTOU window for concurrent
retries. The repository unique key still surfaces the race as a
PostgresException which AZ-353 maps to a clean error. Acceptable for
realistic sequential-retry patterns; recorded in batch report as a
non-blocking observation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes the partial-coverage gap from batch 10. Adds two integration
tests in MigrationTests.cs:
- DedupeSqlCollapsesDuplicatesByLatestUpdatedAt_AZ357_AC2: seeds a
session-scoped temp table with intentional 4-column duplicates
(varying updated_at and id), runs the exact dedupe SQL from
migration 012, asserts only the expected rows survive (newest
updated_at wins; ties broken by largest id).
- NewUniqueConstraintExistsOnFourColumns_AZ357_AC2: queries
pg_indexes against the live DB to assert idx_tiles_unique_location
is a unique 4-column btree and excludes the version column.
Also wires Npgsql 9.0.2 into the integration-tests project, exposes
DB_CONNECTION_STRING + postgres healthcheck dependency to the test
container in docker-compose.tests.yml, and registers the new tests
in both smoke and full suites.
Implementation note: first attempt used CREATE TEMP TABLE
ON COMMIT DROP, which dropped the table immediately because each
Npgsql command runs in its own implicit transaction. Removed
ON COMMIT DROP — session-scoped temps are dropped on connection
close, which is what we want.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-357 — eliminate year-based tile cache expiry (LF-1):
- Migration 012: drop 5-col unique index, dedupe by (lat,lon,zoom,
size) keeping max(updated_at), add new 4-col unique index, make
version column nullable + drop default. Column itself preserved
per coderule (column drops require explicit confirmation; tracked
in AZ-373 / C20).
- TileEntity.Version, TileMetadata.Version, DownloadTileResponse.
Version: int -> int? (HTTP shape preserved; field still in JSON).
- TileService.DownloadAndStoreTilesAsync: drop currentVersion year
computation and the .Where(t => t.Version == currentVersion)
cache filter. BuildTileEntity: drop year arg; write Version=null.
- TileRepository: ON CONFLICT now 4-col; lookup queries
ORDER BY updated_at DESC instead of version DESC.
- Tests: replace inverted BT02b with positive AZ357_AC1
(prior-year cached tile is reused). Add BuildTileEntity_
DoesNotPopulateVersion_AZ357 to enforce the no-write contract.
- 69 unit + 5 smoke + 3 stub-contract integration tests pass.
Cumulative code review (batches 7-9, 7 tasks): VERDICT=PASS.
Report at _docs/03_implementation/reviews/batch_09_review.md.
Zero Critical/High/Medium/Low findings. Architecture baseline
remains clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace 9 nearly-identical catch blocks in
RegionService.ProcessRegionAsync with a single catch (Exception ex)
that delegates to RegionFailureClassifier.Classify, returning a
typed (category, errorMessage) pair. Preserves all original error
messages stored in region summary files; failure-path call into
HandleProcessingFailureAsync is unchanged.
Net source delta: -38 lines in RegionService, +71 lines in new
RegionFailureClassifier (pure static), +10 unit tests covering
each category, precedence, status-code propagation, null guard.
Tests: 68 unit (was 58) + 5 smoke + 3 stub-contract integration
tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-353: Centralize 500 handling via GlobalExceptionHandler /
AddProblemDetails / UseExceptionHandler. Sanitized ProblemDetails
body carries a generic title, RFC9110 type link, and the request's
TraceIdentifier as correlationId; the leaky exception message stays
server-side in the ERR log entry. Strip per-endpoint
try/catch (Exception) wrappers and the unused ILogger<Program>
parameters they served. Preserve the typed ArgumentException catch
in CreateRoute (AC-3). The handler maps BadHttpRequestException
back to its framework-supplied StatusCode so model-binding /
malformed-body failures stay 4xx instead of being promoted to 500.
AZ-354: Extract CorsConfigurationValidator (pure static helpers)
and wire it into Program.cs. Production with empty
CorsConfig:AllowedOrigins and no CorsConfig:AllowAnyOrigin opt-in
now throws InvalidOperationException at host startup. Development
keeps the permissive default but logs a warning post-build. Adds
the explicit CorsConfig:AllowAnyOrigin escape hatch.
AZ-356: GetSatelliteTilesByMgrs and UploadImage now return
Results.Problem(StatusCode 501) with ProblemDetails. Added
.ProducesProblem(501) so swagger.json documents the
not-implemented status.
Tests: SatelliteProvider.Tests now references SatelliteProvider.Api
(downward, idiomatic) so unit tests can reach the new helpers.
+9 CorsConfigurationValidator unit tests, +3
GlobalExceptionHandler unit tests, +3 StubAndErrorContractTests
integration tests (added to smoke + full suites).
58/58 unit + 5/5 smoke + 3/3 stub-contract pass.
Code review verdict: PASS.
Batch report: _docs/03_implementation/batch_08_report.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
AZ-351: Resolve ILogger<DatabaseMigrator> directly from DI in
Program.cs instead of casting ILogger<Program> (which always
returned null). Migrator now logs through Serilog at startup.
AZ-352: Drop empty catch in
RouteProcessingService.ExtractTileCoordinatesFromFilename. Convert
the method from private static to internal instance so it can use
the existing _logger (per coderule: side-effecting code must not be
static). Add typed null-guard via ArgumentNullException.ThrowIfNull
so unexpected exceptions propagate. Adds InternalsVisibleTo on the
RouteManagement csproj for SatelliteProvider.Tests, plus 4 unit
tests in RouteProcessingServiceTests.cs covering AC-1 (valid /
malformed / non-numeric) and AC-2 (null path propagation).
AZ-363: Delete _totalEnqueued / _totalDequeued fields and the two
non-atomic ++ writes in RegionRequestQueue. Fields were write-only
dead code and a thread-safety hazard.
Tests: 44/44 unit + 5/5 smoke (scripts/run-tests.sh --smoke).
Code review verdict: PASS, 0 findings, 0 auto-fix attempts.
Batch report: _docs/03_implementation/batch_07_report.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds _docs/04_refactoring/02-coupling-refactoring/FINAL_report.md
recording goal achievement, baseline-vs-final metrics, batch
summaries, and lessons learned. Advances autodev state from
Step 8 (Refactor) to Step 9 (New Task) — Phase A baseline setup
is now complete; Phase B feature cycle starts on next invocation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase C of architecture coupling refactor (epic AZ-309). Closes the
last baseline finding (F5 — DataAccess incorrectly documented as
importing Common) and synchronizes the rest of _docs/02_document/
with the post-split project layout from AZ-312/313/314:
- module-layout.md: per-component sections for the three new csprojs
with explicit ProjectReferences and the no-cross-sibling-reference
invariant the split enforces.
- architecture.md: components and internal-communication tables
updated to show calls flow through Common interfaces.
- architecture_compliance_baseline.md: F1..F5 marked Resolved with
task IDs and commit refs; baseline summary now 0 findings.
- diagrams/components.md, components/03_tile_downloader/description.md,
modules/{common_interfaces,services_tile_service,
services_google_maps_downloader,tests_unit}.md updated for the
split, RateLimitException relocation, and new ITileService methods.
Documentation-only batch — no code, no tests, no build changes.
Epic AZ-309 complete (6 tasks across 3 batches).
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase B of architecture coupling refactor (epic AZ-309). Replaces
the monolithic SatelliteProvider.Services with three per-component
csprojs to add a compiler-enforced module boundary (resolves F4):
- SatelliteProvider.Services.TileDownloader
- SatelliteProvider.Services.RegionProcessing
- SatelliteProvider.Services.RouteManagement
DI registrations relocated into per-component AddTileDownloader /
AddRegionProcessing / AddRouteManagement extension methods called
from Program.cs. RateLimitException moved to Common/Exceptions/ to
keep the three new csprojs as siblings (no Region->TileDownloader
ProjectReference). Dockerfiles and consumer csprojs (Api, Tests)
rewired to the new project paths. No DI lifetime or hosted-service
order changes.
Build: 0 warn, 0 err. Unit tests: 40/40. Smoke integration: green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move cache+DB+download logic for /tiles/{z}/{x}/{y} and
/api/satellite/tiles/latlon out of Program.cs into TileService.
Endpoints now inject only ITileService + ILogger. Service owns
IMemoryCache (1h absolute / 30min sliding preserved). Added
TileBytes DTO; ITileService gains GetOrDownloadTileAsync and
DownloadAndStoreSingleTileAsync. 5 new unit tests cover cache
hit, repo hit, downloader fallback, and AZ-311 happy + error.
Build clean (0/0), unit suite 40/40. Resolves architecture
baseline F3 in code (docs handled by AZ-315).
Co-authored-by: Cursor <cursoragent@cursor.com>
- Baseline metrics, list of changes, and analysis (research findings,
refactoring roadmap) for the coupling refactor run
- Six task specs AZ-310..AZ-315 covering endpoint routing through
ITileService, Services csproj split, consumer rewire, DI extension
methods, and docs sync
- Existing test coverage assessment for Phase 3 safety net gate
- Dependencies table updated with the refactor block
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a fast integration profile so Step 7 (and future autodev
re-entries) can verify the full stack in ~2 min instead of ~15 min,
without losing access to the long-running coverage when needed.
- TestRunMode.cs: smoke flag + tightened poll/timeout values.
- Program.cs: env var INTEGRATION_TESTS_MODE / --smoke|--full CLI
switch; smoke runs Tile + 200m region + simple route + ZIP route +
Security; full keeps the existing sequence.
- RegionTests / ExtendedRouteTests: read timeouts from TestRunMode
instead of hardcoding 120/180/360.
- docker-compose.tests.yml: forwards INTEGRATION_TESTS_MODE to the
integration-tests container (default 'full').
- scripts/run-tests.sh: adds --unit-only / --smoke / --full flags,
loads .env automatically, fails fast if GOOGLE_MAPS_API_KEY is
missing.
Step 7 result: all tests passed in 111.86 s wall-clock (35/35 unit +
5/5 smoke integration scenarios incl. SEC-01..04). Report saved to
_docs/03_implementation/test_run_step7.md.
State advanced to Step 8 (Refactor).
Co-authored-by: Cursor <cursoragent@cursor.com>
Final implementation report for the test step. All six test tasks
(AZ-285..AZ-290) completed across three batches. Code review verdicts:
all PASS_WITH_WARNINGS. Run-Tests handoff recorded for Step 7 per
implement skill Step 16.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add SatelliteProvider.DataAccess project reference to test csproj
(enables mocking ITileRepository / IRegionRepository / IRouteRepository)
- Replace DummyTest placeholder with InfrastructureTests covering:
* All mockable interfaces (ISatelliteDownloader, repos, queue, services)
can be mocked via Moq
* TileService can be constructed with mocked dependencies
* Test coordinate fixtures load with expected values
- Add Fixtures/TestCoordinates.cs with REG-01..REG-03 + ROUTE-01/04/06
shared test data
- Archive AZ-285 to _docs/02_tasks/done/
- Batch 1 review report: PASS_WITH_WARNINGS (Low/Spec-Gap deferred AC-2,
Low/Maintainability pre-existing FluentAssertions 8.x license note)
Verification: docker dotnet test run — 4/4 tests pass in 2.35s.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replaced HARBOR_USER and HARBOR_TOKEN with REGISTRY_USER and REGISTRY_TOKEN for improved clarity and consistency. Adjusted Docker login command and updated image tagging to reflect the new registry path under 'azaion'. This change enhances the pipeline's ability to authenticate and push images to the correct registry.