# List of Changes

**Run**: 03-code-quality-refactoring
**Mode**: automatic
**Source**: self-discovered
**Date**: 2026-05-10

## Summary

Twenty-seven changes spanning five concerns: (1) silent-error-suppression and information-leakage that violate project rules, (2) god-class / 165-LOC-method SRP violations that grew during the original implementation, (3) duplicated logic that the recent `02-coupling-refactoring` exposed (Haversine, CSV writer, image stitcher, magic tolerance, Earth constants), (4) magic strings and numbers that should be enums or config, and (5) tooling gaps (no formatter, no analyzer, no coverage). C23–C27 were added during Phase 2 hardening track A (Tech Debt sweep). All changes preserve HTTP shape; C06 + C09 are the only ones that materially change DB or API behavior and are flagged for deliberate execution.

Severity legend: **Critical** = correctness or security violation, **High** = silent failure mode or expensive bug, **Medium** = maintainability/duplication, **Low** = polish/tooling.

## Changes

### C01: Fix null logger passed to DatabaseMigrator at startup
- **File(s)**: `SatelliteProvider.Api/Program.cs:82-83`
- **Problem**: `var logger = app.Services.GetRequiredService<ILogger<Program>>(); var migrator = new DatabaseMigrator(connectionString, logger as ILogger<DatabaseMigrator>);` — the `as` cast between unrelated generic `ILogger<T>` instantiations always returns `null`. `DatabaseMigrator` therefore runs with a null logger; if its constructor doesn't null-check it, migration failures are invisible.
- **Change**: Resolve `ILogger<DatabaseMigrator>` directly from the service provider.
- **Rationale**: Migration failure is the loudest possible startup error and must be observable.
- **Constraint Fit**: No HTTP or schema change; pure DI fix.
- **Severity**: Critical
- **Risk**: low
- **Dependencies**: None

### C02: Remove empty catch block in ExtractTileCoordinatesFromFilename
- **File(s)**: `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs:610-630`
- **Problem**: Empty `catch { }` swallows every parse/IO exception silently and the function returns `(-1, -1)`. Callers treat this as "not stitchable" — the tile is dropped from the route map without trace. Direct violation of `.cursor/rules/coderule.mdc` ("Never suppress errors silently").
- **Change**: Replace the empty catch with explicit catch of `FormatException`/`ArgumentException` only, log at warning level with `_logger`, and propagate everything else.
- **Rationale**: A malformed tile path now yields a visible warning and the operator can act.
- **Constraint Fit**: Behavior identical for valid filenames; only failure modes change.
- **Severity**: Critical
- **Risk**: low
- **Dependencies**: None

### C03: Stop leaking exception messages to API clients
- **File(s)**: `SatelliteProvider.Api/Program.cs:139-143, 170-174, 206-210, 226-230, 245-249, 265-269`
- **Problem**: Every endpoint catches `Exception` and returns `Results.Problem(detail: ex.Message, statusCode: 500)`. The ProblemDetails `detail` is shipped to the client, exposing internal stack-trace fragments, file paths, SQL error text, and Google API error bodies.
- **Change**: Replace per-endpoint `try/catch (Exception)` with a global exception handler (`UseExceptionHandler` + `IExceptionHandler` or middleware) that returns a sanitized `ProblemDetails` (correlation ID + generic title) and logs the full exception server-side with the same correlation ID.
- **Rationale**: Information disclosure is a recurring OWASP top-10 finding; centralizing also kills the boilerplate.
- **Constraint Fit**: HTTP status codes preserved (500). Specific 400 paths (`ArgumentException` in `CreateRoute`) keep their specific handling.
- **Severity**: Critical
- **Risk**: medium (changes response body shape on 500; requires updating any test that asserts on `detail`)
- **Dependencies**: None

### C04: Make CORS strict by default
- **File(s)**: `SatelliteProvider.Api/Program.cs:37-47`
- **Problem**: When `CorsConfig:AllowedOrigins` is empty (the default in many environments) the policy falls through to `policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()`. A misconfigured prod deployment silently exposes the entire surface to any origin.
- **Change**: When `AllowedOrigins` is empty, refuse to start in `Production` and warn loudly in `Development`. Only configure the open policy when the operator explicitly opts in via a config flag (e.g., `CorsConfig:AllowAnyOrigin=true`).
- **Rationale**: Secure defaults; explicit opt-in for permissive CORS.
- **Constraint Fit**: No change to integration tests (they run with explicit `AllowedOrigins`).
- **Severity**: Critical
- **Risk**: low (provided integration tests already specify origins)
- **Dependencies**: None

### C05: Stub endpoints must return 501 Not Implemented
- **File(s)**: `SatelliteProvider.Api/Program.cs:177-180 (GetSatelliteTilesByMgrs), 182-185 (UploadImage)`
- **Problem**: `GetSatelliteTilesByMgrs` returns 200 OK with an empty payload; `UploadImage` returns 200 OK with `Success=false`. Clients can't distinguish "stubbed" from "valid empty result" or "real failure".
- **Change**: Return `Results.StatusCode(501)` (with a brief problem-details body indicating "feature not implemented") until real implementations land. Update OpenAPI metadata accordingly.
- **Rationale**: Honest contract; HTTP semantics.
- **Constraint Fit**: Smoke tests do not exercise these endpoints (per `test_run_step7.md`).
- **Severity**: High
- **Risk**: low
- **Dependencies**: None

### C06: Drop the `Version` concept; always return the latest tile per (lat, lon, zoom)
- **File(s)**: `SatelliteProvider.Services.TileDownloader/TileService.cs:42, 45, 113, 135`; `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` (region/lookup queries); `SatelliteProvider.DataAccess/Migrations/` (new migration); `SatelliteProvider.DataAccess/Models/TileEntity.cs`
- **Problem**: `var currentVersion = DateTime.UtcNow.Year` becomes the row's `Version`. On every Jan 1 UTC the year flips, the region path filters out all prior-year tiles via `existingTiles.Where(t => t.Version == currentVersion)`, and the entire cache effectively expires. The concept of "version" is also unused anywhere else as a cache-control lever. See `logical_flow_analysis.md#LF-1`.
- **Change**: Remove versioning from the tile cache logic entirely. The system becomes "the latest stored tile for a given `(latitude, longitude, zoom_level)` is the cached tile":
  1. Delete the `t.Version == currentVersion` filter in `TileService.DownloadAndStoreTilesAsync`.
  2. Stop writing `currentVersion` to `TileEntity.Version` in `BuildTileEntity`.
  3. In repository lookups (`GetTilesByRegionAsync`, `GetByTileCoordinatesAsync`), ensure they return the most recently updated row when duplicates exist (`ORDER BY updated_at DESC LIMIT 1` for single-tile lookups; for region lookups, deduplicate on `(latitude, longitude, zoom_level, tile_size_meters)` keeping the latest).
  4. To prevent unbounded duplicate accumulation: change `InsertAsync` semantics to "upsert by `(latitude, longitude, tile_zoom, tile_size_meters)`". `TileRepository.InsertAsync` already uses `ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters, version) DO UPDATE`; the migration drops the existing 5-column unique constraint and replaces it with a 4-column one (without `version`), then dedupes pre-existing duplicate rows by keeping the highest `updated_at`.
  5. The `version` column itself is left in the table (per `coderule.mdc`: do not rename or drop columns without explicit confirmation). It becomes a vestigial nullable column that the new code ignores. **Optional follow-up** (separate ticket, not part of this change): drop the column once we are sure no consumer reads it.
- **Rationale**: One concept (latest), one row per geographic tile cell, no time-of-year cliff.
- **Constraint Fit**: HTTP shape preserved (`DownloadTileResponse.Version` stays in the JSON; for new responses the value is whatever the row carries — old rows keep their year value, new rows get `0` or `null` per migration default). DB column kept per `coderule.mdc`; the migration only adds a unique constraint and dedupes existing rows. Smoke test will see a single `tiles` row per geographic position regardless of run date.
- **Severity**: High
- **Risk**: medium (migration must dedupe existing tile rows safely; needs review before running in environments with real data)
- **Dependencies**: None (C20 follows from this change)

### C07: Consolidate the 9-way catch ladder in `RegionService.ProcessRegionAsync`
- **File(s)**: `SatelliteProvider.Services.RegionProcessing/RegionService.cs:148-197`
- **Problem**: Nine nearly-identical `catch` blocks (TaskCanceledException × 3, OperationCanceledException × 2, RateLimitException, HttpRequestException, Exception × 1) all do the same thing: build an `errorMessage` and call `HandleProcessingFailureAsync`. ~50 LOC of duplication; any new failure category will require touching all of them.
- **Change**: Extract a single `try/catch (Exception ex)` that delegates to a small `ClassifyRegionFailure(ex, timeoutCts, cancellationToken)` helper returning the category + human message. The catch block then calls `HandleProcessingFailureAsync` once.
- **Rationale**: Same observable behavior; one reason to change the failure-handling code.
- **Constraint Fit**: All currently-tested failure paths preserved (RegionTests covers timeout + rate-limit indirectly).
- **Severity**: Medium
- **Risk**: low (covered by existing unit + integration tests)
- **Dependencies**: None

### C08: Replace service-locator pattern in `RouteProcessingService`
- **File(s)**: `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs:18-22, 105-109, 165-169`
- **Problem**: Constructor injects `IServiceProvider`; the body creates a scope and resolves `IRegionService` per pending route iteration. `IRegionService` is registered as a singleton (verified in `RegionProcessingServiceCollectionExtensions`), so the scope creation is unnecessary and the service-locator pattern hides the real dependency from the constructor signature.
- **Change**: Inject `IRegionService` directly. Remove `IServiceProvider` field and the two `using var scope = _serviceProvider.CreateScope();` blocks.
- **Rationale**: Honest dependency declaration; faster (no scope allocation per loop); removes a service-locator footgun.
- **Constraint Fit**: No behavior change as long as `IRegionService` remains a singleton. If a future change makes it scoped, switch to `IServiceScopeFactory` (still better than `IServiceProvider`).
- **Severity**: Medium
- **Risk**: low
- **Dependencies**: None

### C09: Define an idempotency contract for caller-supplied GUIDs
- **File(s)**: `SatelliteProvider.Api/Program.cs:187-211 (RequestRegion), 233-250 (CreateRoute)`; `SatelliteProvider.Services.RegionProcessing/RegionService.cs:38-71`; `SatelliteProvider.Services.RouteManagement/RouteService.cs:27-211`
- **Problem**: Both endpoints accept `request.Id` from the caller and `INSERT` blindly. A retried POST hits a unique-key conflict at the DB and surfaces as a 500 (with a leaky message — see C03). The client has no way to know whether their first POST succeeded.
- **Change**: On `INSERT` conflict for a known `Id`, return the existing resource state (200 OK) rather than 500. Document this idempotency in the OpenAPI spec.
- **Rationale**: Standard idempotency for retry-safe POSTs. Reduces support load and prevents duplicate background work.
- **Constraint Fit**: HTTP shape preserved on success; 500-on-duplicate becomes 200-on-duplicate (a strict improvement). No DB schema change.
- **Severity**: High
- **Risk**: medium (behavior change at the contract boundary — needs decompose ticket reviewed by user)
- **Dependencies**: C03 (so the new path doesn't go through the leaky 500)

### C10: Remove non-atomic, write-only counters from `RegionRequestQueue`
- **File(s)**: `SatelliteProvider.Services.RegionProcessing/RegionRequestQueue.cs:12-13, 28, 38`
- **Problem**: `_totalEnqueued++` and `_totalDequeued++` are non-atomic on `int` from concurrent producers / consumers, and the fields are never read anywhere in the codebase.
- **Change**: Delete the two `int` fields and the two `++` lines. No replacement.
- **Rationale**: Removes both the silent thread-safety bug and the dead code. Telemetry, if needed later, will be added via a proper counter abstraction (e.g., `Meter`/`Counter<long>`) rather than ad-hoc fields.
- **Constraint Fit**: No public-API impact (the fields are private).
- **Severity**: Medium
- **Risk**: low
- **Dependencies**: None

### C11: Decompose `RouteProcessingService` (god class, ~750 LOC)
- **File(s)**: `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs` (entire file)
- **Problem**: A single `BackgroundService` does: queue polling, region matching, CSV parsing, summary writing, image stitching, geofence-rectangle drawing, route-cross drawing, ZIP creation, and cleanup. Six+ responsibilities, ~750 LOC, and the file even hosts a public `TileInfo` POCO at the bottom.
- **Change**: Extract into orchestrator + collaborators in `Services.RouteManagement`:
  - `RouteRegionMatcher` (pure: route points + completed regions → ordered region list)
  - `RouteCsvWriter` (writes route_<id>_ready.csv from `IEnumerable<TileInfo>`)
  - `RouteSummaryWriter` (writes route_<id>_summary.txt; includes the StringBuilder block)
  - `RouteImageRenderer` (image stitching + cross/border drawing)
  - `TilesZipBuilder` (ZIP archive creation; resolves entry names)
  - `RegionFileCleaner` (deletes per-region CSV/summary/stitched files)
  - `TileInfo` moves to its own file under `Services.RouteManagement` or `Common/DTO/`
  - `RouteProcessingService` becomes a thin orchestrator that polls and dispatches.
- **Rationale**: SRP, testability (each helper unit-testable without a queue), and the file becomes navigable.
- **Constraint Fit**: Same `BackgroundService` lifecycle, same DB writes, same output files.
- **Severity**: Medium
- **Risk**: medium (large file, but the smoke + unit suites cover the end-to-end behavior)
- **Dependencies**: C13 (shared Haversine), C14 (shared stitcher) — those extractions feed into this.

### C12: Decompose `RouteService.CreateRouteAsync` (165-LOC method)
- **File(s)**: `SatelliteProvider.Services.RouteManagement/RouteService.cs:27-211`
- **Problem**: One method does input validation, point-graph construction (with `GeoUtils.CalculateIntermediatePoints`), route entity persistence, route-points persistence, geofence polygon validation (4 separate validation rules), geofence grid generation, geofence region requests, and response mapping. Five distinct responsibilities; the validation alone is ~25 LOC of nested `if` chains.
- **Change**: Extract:
  - `RouteValidator` (all `ArgumentException`-throwing checks; aggregates errors instead of short-circuiting on the first)
  - `RoutePointGraphBuilder` (interpolation + sequence numbering — pure, easy to unit-test)
  - `GeofenceGridCalculator` (NW/SE → list of region centers — already a private method, just promote and unit-test it)
  - `RouteResponseMapper` (entity → DTO; eliminates duplication with `GetRouteAsync`)
- **Rationale**: SRP; aggregated validation is a UX win for callers.
- **Constraint Fit**: Same persistence calls and same response shape; aggregated validation returns 400 the same as today (still via `ArgumentException` or a typed `ValidationException`).
- **Severity**: Medium
- **Risk**: low (covered by `RouteServiceTests` unit suite + integration smoke)
- **Dependencies**: None

### C13: Consolidate duplicate Haversine + tile-coord parsing into `Common/Utils`
- **File(s)**: `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs:596-608, 610-630`; `SatelliteProvider.Common/Utils/GeoUtils.cs`
- **Problem**: `RouteProcessingService.CalculateDistance(lat1, lon1, lat2, lon2)` re-implements `GeoUtils.CalculateDistance(GeoPoint, GeoPoint)`. `ExtractTileCoordinatesFromFilename` is a parser tied to the filename pattern "tile_{ts}_{x}_{y}.jpg", which is generated by `StorageConfig.GetTileFilePath` in another assembly — the two are coupled by string convention only.
- **Change**: (a) Delete `RouteProcessingService.CalculateDistance`; call `GeoUtils.CalculateDistance` with `GeoPoint` instances. (b) Move `ExtractTileCoordinatesFromFilename` next to `StorageConfig.GetTileFilePath` (or onto `StorageConfig` itself) so the parser and the writer live together — this is a public API contract pair.
- **Rationale**: Single source of truth for both formulas; coupling becomes structural rather than by-string-convention.
- **Constraint Fit**: Same behavior; no public API change beyond moving a static method.
- **Severity**: Medium
- **Risk**: low
- **Dependencies**: None

### C14: Extract shared `TileGridStitcher` for region + route image generation
- **File(s)**: `SatelliteProvider.Services.RegionProcessing/RegionService.cs:240-321`; `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs:453-570`
- **Problem**: Both files implement "place tiles in a grid by (TileX, TileY) and overlay markers". The basic placement loop, min/max calculation, and `Image.LoadAsync<Rgb24>` per tile are duplicated. Differences are only the overlays (region: red cross at center; route: yellow geofence rectangles + red crosses at route points).
- **Change**: Extract `TileGridStitcher` in `Common` (or a new `SatelliteProvider.Imaging` project) with:
  - `Task<Image<Rgb24>> StitchAsync(IEnumerable<TilePlacement> tiles, CancellationToken ct)`
  - Overlay primitives: `DrawCross(Image, Point, Color, ArmLength)` and `DrawRectangleBorder(Image, Rect, Color, Thickness)` exposed as instance methods.
- **Rationale**: Single implementation; testable in isolation.
- **Constraint Fit**: Output images identical pixel-for-pixel for the existing test scenarios (verified during execution).
- **Severity**: Medium
- **Risk**: medium (image output is checked by integration tests; a pixel diff would catch regressions)
- **Dependencies**: C11 (route-side caller is restructured at the same time)

### C15: Extract shared `TileCsvWriter`
- **File(s)**: `SatelliteProvider.Services.RegionProcessing/RegionService.cs:323-334`; `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs:388-404`
- **Problem**: Both files write the same CSV header (`latitude,longitude,file_path`) with the same ordering rule (`OrderByDescending(t.Latitude).ThenBy(t.Longitude)`) and the same `F6` numeric format. Two near-identical writers.
- **Change**: Extract `TileCsvWriter` in `Common` with one method `WriteAsync(string path, IEnumerable<TileRecord> tiles, CancellationToken ct)`.
- **Rationale**: One place to change the CSV contract.
- **Constraint Fit**: Output bytes identical.
- **Severity**: Medium
- **Risk**: low
- **Dependencies**: None

### C16: Move inline DTOs from `Program.cs` to `Common/DTO/`
- **File(s)**: `SatelliteProvider.Api/Program.cs:272-353 (GetSatelliteTilesResponse, SatelliteTile, UploadImageRequest, SaveResult, DownloadTileResponse, RequestRegionRequest)`
- **Problem**: Six request/response DTOs and one Swagger filter live at the bottom of `Program.cs`. SRP: API host file should only wire endpoints; data shapes belong in `Common/DTO/`.
- **Change**: Move the six DTOs to `SatelliteProvider.Common/DTO/`; move `ParameterDescriptionFilter` to a new `SatelliteProvider.Api/Swagger/ParameterDescriptionFilter.cs`.
- **Rationale**: Same SRP discipline applied to the rest of the codebase.
- **Constraint Fit**: Public OpenAPI shape unchanged; only namespaces change.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: None

### C17: Replace status / point-type magic strings with enums (+ AC RT2 doc update)
- **File(s)**: `SatelliteProvider.Services.RegionProcessing/RegionService.cs:49,90,140,209` ("queued"/"processing"/"completed"/"failed"); `SatelliteProvider.Services.RouteManagement/RouteService.cs:66,100` ("start"/"end"/"action"/"intermediate"); `RouteProcessingService.cs:138-140`; `_docs/00_problem/acceptance_criteria.md` RT2
- **Problem**: Status/type values are bare strings written to and compared from multiple places. Typos at compile time become runtime bugs. Acceptance criterion RT2 is also out of sync with the lived code: AC says point types are `original` / `intermediate`, but code uses 4 values: `start` / `end` / `action` / `intermediate` — and tests, helpers, and DB rows all rely on the 4-value model.
- **Change**: Introduce two enums in `SatelliteProvider.Common/Enums/`:
  - `RegionStatus { Queued, Processing, Completed, Failed }`
  - `RoutePointType { Start, End, Action, Intermediate }` (user-confirmed canonical names — option α)
  Persist to DB as the existing lowercase string values via a Dapper `SqlMapper.AddTypeHandler<T>(new EnumStringTypeHandler<T>())` registered once at startup. **Update `_docs/00_problem/acceptance_criteria.md` RT2** so the wording matches the 4-value reality: "Point types correctly assigned: `start` for the first user-supplied waypoint, `end` for the last, `action` for middle waypoints, `intermediate` for generated fill-ins."
- **Rationale**: Compile-time safety; refactor-friendly; ends the AC-vs-code drift in the same ticket that touches both.
- **Constraint Fit**: DB column types and stored values are identical; no migration needed. AC update is documentation, not a behavior change. (Resolves Constraint K8 from `research_findings.md`.)
- **Severity**: Medium
- **Risk**: low (Dapper type handlers are well-established)
- **Dependencies**: None

### C18: Move hardcoded magic numbers into `ProcessingConfig` / `MapConfig`
- **File(s)**: `RegionService.cs:94` (5 min timeout); `RouteService.cs:15` (200 m point spacing); `RouteProcessingService.cs:22` (5 s polling); `RouteService.cs:154`, `GoogleMapsDownloaderV2.cs:252` (`0.0001` lat/lon tolerance); `GoogleMapsDownloaderV2.cs:18-21` (TILE_SIZE_PIXELS, MAX_RETRY_DELAY_SECONDS, BASE_RETRY_DELAY_SECONDS, ALLOWED_ZOOM_LEVELS); `TileService.cs:152` (TileSizePixels = 256). Also: forward `CancellationToken` from `Program.cs:GetTileByLatLon` (line 150) into `DownloadAndStoreSingleTileAsync` (LF-2).
- **Problem**: Operational levers are baked into source. Tuning means a redeploy.
- **Change**: Add `ProcessingConfig.RegionProcessingTimeout`, `ProcessingConfig.RouteProcessingPollInterval`, `ProcessingConfig.MaxRoutePointSpacingMeters`, `ProcessingConfig.LatLonTolerance`, `MapConfig.TileSizePixels`, `MapConfig.AllowedZoomLevels`, `MapConfig.RetryBaseDelaySeconds`, `MapConfig.RetryMaxDelaySeconds`. Default values match current literals so behavior is unchanged.
- **Rationale**: Configurable without redeploy; a single source of truth for `0.0001`-style tolerances.
- **Constraint Fit**: Defaults preserve all existing behavior.
- **Severity**: Medium
- **Risk**: low
- **Dependencies**: None

### C19: Add formatter, analyzer, and coverage tooling
- **File(s)**: solution root, all `*.csproj`
- **Problem**: No `dotnet format` gate, no Roslyn analyzers beyond defaults, no Coverlet for coverage. Style drift and easy-to-miss bugs slip through.
- **Change**: Add `Microsoft.CodeAnalysis.NetAnalyzers` and `coverlet.collector` to the test projects; add a root `.editorconfig` (if not already present) with the team's preferences and wire `dotnet format --verify-no-changes` into the `scripts/run-tests.sh`. Adopt one initial analyzer ruleset (CA1001, CA1051, CA2007, CA2227 etc.) at warning severity, not error, to avoid a flood.
- **Rationale**: Catches at build time what would otherwise become future findings.
- **Constraint Fit**: Tooling-only change; no runtime behavior.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: None

### C20: Clarify `MapsVersion` semantics
- **File(s)**: `SatelliteProvider.Services.TileDownloader/TileService.cs:154`; `SatelliteProvider.DataAccess` `tiles.maps_version` column; `SatelliteProvider.Common/DTO/TileMetadata.cs`; `SatelliteProvider.Api/Program.cs:DownloadTileResponse`
- **Problem**: `MapsVersion = $"downloaded_{now:yyyy-MM-dd}"` — the field name says "version" but the value is a creation-date label. The actual cache-key version is the integer `Version` column (currently the year — see C06). Confusing.
- **Change**: Either (a) drop `MapsVersion` from the row entirely if `Version` (post-C06) carries the version concept, or (b) document `MapsVersion` as a free-form provider-tag and keep it for forensics. Decide alongside C06.
- **Rationale**: One concept, one name.
- **Constraint Fit**: DB column kept (no rename per `coderule.mdc`); only the value-source changes if option (a) drops it.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: C06

### C21: Register typed/named `HttpClient` for Google Maps in DI
- **File(s)**: `SatelliteProvider.Api/Program.cs:31`; `SatelliteProvider.Services.TileDownloader/GoogleMapsDownloaderV2.cs:51, 107, 369`
- **Problem**: `GoogleMapsDownloaderV2` calls `_httpClientFactory.CreateClient()` inside three different code paths (session-token fetch, single-tile download, batch-tile download retry lambda) and sets `User-Agent` per call. The factory pools `HttpMessageHandler`s correctly, but the per-call header setup is duplicated and error-prone.
- **Change**: Register a typed/named client (`AddHttpClient("GoogleMapsTiles", c => { c.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); ... })` with a sensible timeout) and inject `IHttpClientFactory` to call `CreateClient("GoogleMapsTiles")`.
- **Rationale**: One configuration point; consistent timeouts/headers; no duplicated setup.
- **Constraint Fit**: Same outbound HTTP behavior.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: None

### C22: Optimize O(N²) existing-tile lookup
- **File(s)**: `SatelliteProvider.Services.TileDownloader/GoogleMapsDownloaderV2.cs:245-265`
- **Problem**: For each grid cell, `existingTiles.FirstOrDefault(t => Math.Abs(t.Latitude - tileCenter.Lat) < 0.0001 && ...)` is a linear scan. For a 200 m × zoom 18 region (~16 tiles) this is fine, but for large regions it grows quadratically.
- **Change**: Build a `HashSet<(int x, int y, int z)>` of the existing tiles once, then membership-test by tile coordinates — exact, no tolerance needed (tile coordinates are integers per zoom level).
- **Rationale**: O(N) instead of O(N²); also removes one of the two uses of the magic `0.0001` (the other one — geofence polygon check at `RouteService.cs:154` — is a real lat/lon tolerance and stays).
- **Constraint Fit**: Behavior identical for any input that already produces correct output today.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: C18 (the remaining `0.0001` becomes a config value)

### C23: Delete unused `FindExistingTileAsync`
- **File(s)**: `SatelliteProvider.DataAccess/Repositories/ITileRepository.cs`, `SatelliteProvider.DataAccess/Repositories/TileRepository.cs:51-76`
- **Problem**: `FindExistingTileAsync(latitude, longitude, tileSizeMeters, zoomLevel, version)` is declared on the interface and implemented in the repository, but no caller exists in the codebase (verified by grep — only docs and the implementation match). Dead code that also takes the obsolete `version` argument C06 is removing.
- **Change**: Delete the method from `ITileRepository` and `TileRepository`. No replacement.
- **Rationale**: Per `coderule.mdc` ("If a file, class, or function has no remaining usages — delete it"). Removes a coupling point to the version concept being phased out by C06.
- **Constraint Fit**: No public-API impact (interface is internal to the solution).
- **Severity**: Low
- **Risk**: low (verify with one final grep before deletion)
- **Dependencies**: None

### C24: Consolidate Earth-geometry constants and the magic 111000
- **File(s)**: `SatelliteProvider.Common/Utils/GeoUtils.cs`; `SatelliteProvider.DataAccess/Repositories/TileRepository.cs:82-91`; `SatelliteProvider.Services.TileDownloader/GoogleMapsDownloaderV2.cs:18, 142`
- **Problem**: Three Earth-related constants drift across the codebase:
  - `GeoUtils.EARTH_RADIUS = 6378137` (m)
  - `GoogleMapsDownloaderV2.CalculateTileSizeInMeters: EARTH_CIRCUMFERENCE_METERS = 40075016.686`
  - `TileRepository.GetTilesByRegionAsync: EARTH_CIRCUMFERENCE_METERS = 40075016.686` (duplicate of the above)
  - `TileRepository.GetTilesByRegionAsync: 111000.0` (meters per degree latitude approximation, twice)
  - `TILE_SIZE_PIXELS = 256` (TileRepository:83, GoogleMapsDownloaderV2:18, hard-coded `256` in TileService:152)
- **Change**: Move the Earth constants and the per-degree-latitude conversion into `GeoUtils` as named `public const`s (e.g., `EarthRadiusMeters`, `EarthEquatorialCircumferenceMeters`, `MetersPerDegreeLatitude`). Move `TileSizePixels` to `MapConfig` (also see C18). Replace all duplicate literal sites with the new named constants.
- **Rationale**: One source of truth; if/when ellipsoid choice or tile-pixel-size changes, one edit instead of five.
- **Constraint Fit**: Numerically identical results.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: C18 (TileSizePixels move into config)

### C25: Remove unused `_logger` fields from repositories — or use them
- **File(s)**: `SatelliteProvider.DataAccess/Repositories/TileRepository.cs:11`, `RegionRepository.cs:11`, `RouteRepository.cs` (same pattern)
- **Problem**: Each repository constructor accepts and stores `ILogger<TRepo>`, but the field is never read. Dead injection — adds DI cost and noise without value.
- **Change**: Pick one path:
  - (a) **Delete** the field, the constructor parameter, and update DI registrations in `Program.cs` accordingly.
  - (b) **Use** them: log slow queries (e.g., emit a `LogWarning` if a query takes more than `n` ms) — gives operational visibility for free.
  - Recommended: (b) for `TileRepository.GetTilesByRegionAsync` only (the one that does meaningful work) and (a) everywhere else. The choice is light enough to flip during implementation.
- **Rationale**: Either dead code is removed, or the logger pulls its weight.
- **Constraint Fit**: No public-API change either way.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: None

### C26: De-duplicate repository SELECT column lists
- **File(s)**: `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` (4 SELECTs with same column-mapping clause), `RegionRepository.cs` (2 SELECTs), `RouteRepository.cs`
- **Problem**: The same `id, tile_zoom as TileZoom, tile_x as TileX, ...` column list is repeated up to four times per repository. Every new column must be added in lockstep across all SELECTs; easy to drift.
- **Change**: Extract per-repository `private const string ColumnList = "..."` and interpolate into each query (`$"SELECT {ColumnList} FROM tiles WHERE ..."`). Lightweight; avoids pulling in `Dapper.Contrib` or a micro-ORM.
- **Rationale**: One place to add a column.
- **Constraint Fit**: Generated SQL is byte-for-byte identical.
- **Severity**: Low
- **Risk**: low
- **Dependencies**: None

### C27: Delete `GeoUtils.CalculatePolygonDiagonalDistance` (alias of `CalculateDistance`)
- **File(s)**: `SatelliteProvider.Common/Utils/GeoUtils.cs:129-132`
- **Problem**: `CalculatePolygonDiagonalDistance(GeoPoint nw, GeoPoint se)` returns `CalculateDistance(nw, se)` — pure alias, no callers in the codebase (grep confirms). Adds API surface for nothing.
- **Change**: Delete the method. If any caller appears in the future, they call `CalculateDistance` directly.
- **Rationale**: Per `coderule.mdc` dead-code rule.
- **Constraint Fit**: No public-API impact (no callers).
- **Severity**: Low
- **Risk**: low (verify with one final grep before deletion)
- **Dependencies**: None

## Self-Verification

- [x] Every referenced file in this list exists in the codebase (verified `find . -type f -name "*.cs"`).
- [x] Each change has file path(s), problem, change description, rationale, constraint fit, risk, dependencies, severity.
- [x] Component coverage: Api (C01, C03, C04, C05, C09, C16, C18, C21), Common (C13, C14, C15, C16, C17, C18, C24, C27), DataAccess (C06, C23, C25, C26 — repositories + new migration), Services.TileDownloader (C06, C18, C20, C21, C22, C24), Services.RegionProcessing (C07, C09, C10, C14, C15, C17, C18, C25, C26), Services.RouteManagement (C02, C08, C09, C11, C12, C13, C14, C15, C17, C18, C25, C26), Documentation (C06, C17, C20 — restrictions.md / acceptance_criteria.md updates), Tooling (C19).
- [x] Logical-flow analysis completed and saved at `discovery/logical_flow_analysis.md`; all flow findings (LF-1, LF-2, LF-3, LF-4, LF-5, LF-6) are folded into specific change IDs above.
- [x] No silent data loss claims left unaddressed (LF-1 → C06, LF-6 → C02).
- [x] Architecture Vision compliance: none of the changes contradict the H2 vision section in `architecture.md` (verified) — the recent `02-coupling` cemented module boundaries; this run only operates within them.
